@antglobal/developer-harness 0.0.0 → 0.0.1791614784-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -2
- package/cli.mjs +58 -0
- package/dist/assets/CodeEditorPanel-B2t5PDE9.js +1 -0
- package/dist/assets/abap-08VXUWAP.js +1 -0
- package/dist/assets/apex-BWPQTe0t.js +1 -0
- package/dist/assets/azcli-Bc_sGQ0U.js +1 -0
- package/dist/assets/bat-i0X4ZdIN.js +1 -0
- package/dist/assets/bicep-B5-_aFwp.js +2 -0
- package/dist/assets/cameligo-DMUM7wLl.js +1 -0
- package/dist/assets/clojure-Cm7r79vr.js +1 -0
- package/dist/assets/codicon-Brq4_Ui5.ttf +0 -0
- package/dist/assets/coffee-Ba7i2nA0.js +1 -0
- package/dist/assets/cpp-C7h46wYY.js +1 -0
- package/dist/assets/csharp-BKxtCVv1.js +1 -0
- package/dist/assets/csp-bTuwJoIa.js +1 -0
- package/dist/assets/css-DIMkf-bt.js +3 -0
- package/dist/assets/css.worker-B_vtbeex.js +96 -0
- package/dist/assets/cssMode-BR5yXBkM.js +1 -0
- package/dist/assets/cypher-CVaqCwHa.js +1 -0
- package/dist/assets/dart-onAF5SnQ.js +1 -0
- package/dist/assets/dockerfile-DZFCIeNp.js +1 -0
- package/dist/assets/ecl-D05T4iGw.js +1 -0
- package/dist/assets/editor-8AbsNvUX.css +1 -0
- package/dist/assets/editor.api-CSeSpqfm.js +847 -0
- package/dist/assets/editor.worker-0g2wMcbF.js +33 -0
- package/dist/assets/elixir-6RTg0lbw.js +1 -0
- package/dist/assets/flow9-C5_-GSwl.js +1 -0
- package/dist/assets/freemarker2-ClvA381c.js +3 -0
- package/dist/assets/fsharp-C8Ef5oNN.js +1 -0
- package/dist/assets/go-C-y9NEjX.js +1 -0
- package/dist/assets/graphql-fmXr3nnJ.js +1 -0
- package/dist/assets/handlebars-DzprBoGe.js +1 -0
- package/dist/assets/hcl-CpzslTdj.js +1 -0
- package/dist/assets/html-BRCp3F4m.js +1 -0
- package/dist/assets/html.worker-C0o__ma4.js +509 -0
- package/dist/assets/htmlMode-KD6TpV_M.js +1 -0
- package/dist/assets/index-DngNMzwB.css +1 -0
- package/dist/assets/index-vX-DeIKH.js +633 -0
- package/dist/assets/ini-sBoK_t0W.js +1 -0
- package/dist/assets/java-BEtHBSE6.js +1 -0
- package/dist/assets/javascript-qOKzv2rz.js +1 -0
- package/dist/assets/json.worker-YsJGeJy7.js +65 -0
- package/dist/assets/jsonMode-C9qxPjIz.js +7 -0
- package/dist/assets/julia-Bri6UV-V.js +1 -0
- package/dist/assets/kotlin-BOotOW0E.js +1 -0
- package/dist/assets/less-B9JPFI3C.js +2 -0
- package/dist/assets/lexon-CfSJPG6W.js +1 -0
- package/dist/assets/liquid-D9HQU4CY.js +1 -0
- package/dist/assets/lspLanguageFeatures-ec6bY6Er.js +4 -0
- package/dist/assets/lua-CsQS60Ue.js +1 -0
- package/dist/assets/m3-D-oSqn_W.js +1 -0
- package/dist/assets/markdown-Cimd5fb3.js +1 -0
- package/dist/assets/mdx-iOtYS5Zc.js +1 -0
- package/dist/assets/mips-CIPQ_RoX.js +1 -0
- package/dist/assets/msdax-DauUninz.js +1 -0
- package/dist/assets/mysql-SOo6toE5.js +1 -0
- package/dist/assets/objective-c-FvmIjYaQ.js +1 -0
- package/dist/assets/pascal-DrH0SRf2.js +1 -0
- package/dist/assets/pascaligo-D-ptJ9y-.js +1 -0
- package/dist/assets/perl-oz_6vUea.js +1 -0
- package/dist/assets/pgsql-DTj74zXo.js +1 -0
- package/dist/assets/php-nr791fC2.js +1 -0
- package/dist/assets/pla-CopQ2nXW.js +1 -0
- package/dist/assets/postiats-43DmfD33.js +1 -0
- package/dist/assets/powerquery-D3hlyOfw.js +1 -0
- package/dist/assets/powershell-DmHpPYUd.js +1 -0
- package/dist/assets/protobuf-C531GsRP.js +2 -0
- package/dist/assets/pug-Z5eAx3Zn.js +1 -0
- package/dist/assets/python-CwtIrHel.js +1 -0
- package/dist/assets/qsharp-DkqhCAOL.js +1 -0
- package/dist/assets/r-BwWrilGY.js +1 -0
- package/dist/assets/razor-6HpXSdqw.js +1 -0
- package/dist/assets/redis-ClamHrr6.js +1 -0
- package/dist/assets/redshift-DT7zqm-g.js +1 -0
- package/dist/assets/restructuredtext-BYgofb2h.js +1 -0
- package/dist/assets/ruby-DezsRK8O.js +1 -0
- package/dist/assets/rust-DdL9SqIa.js +1 -0
- package/dist/assets/sb-CcwsVR0C.js +1 -0
- package/dist/assets/scala-DHpiXF5c.js +1 -0
- package/dist/assets/scheme-BeGwcela.js +1 -0
- package/dist/assets/scss-gp-XZpBa.js +3 -0
- package/dist/assets/shell-CC2rA5mh.js +1 -0
- package/dist/assets/solidity-BEEn4gHE.js +1 -0
- package/dist/assets/sophia-CRfGWb83.js +1 -0
- package/dist/assets/sparql-D_Lu-MrJ.js +1 -0
- package/dist/assets/sql-NEE52Syq.js +1 -0
- package/dist/assets/st-DbInun42.js +1 -0
- package/dist/assets/swift-Bxkupp3x.js +1 -0
- package/dist/assets/systemverilog-Bz4Y3fRF.js +1 -0
- package/dist/assets/tcl-DISqw1ZD.js +1 -0
- package/dist/assets/ts.worker-CGOBEEBw.js +67741 -0
- package/dist/assets/tsMode-C0jLRNgo.js +11 -0
- package/dist/assets/twig-De2hgUGE.js +1 -0
- package/dist/assets/typescript-qziiT50Q.js +1 -0
- package/dist/assets/typespec-B8J7ngcE.js +1 -0
- package/dist/assets/vb-DV3o63ZY.js +1 -0
- package/dist/assets/wf-logo-CSfy9vgQ.png +0 -0
- package/dist/assets/wgsl-DpFanUEy.js +298 -0
- package/dist/assets/workers-BHXUCo0O.js +1 -0
- package/dist/assets/xml-BNLZBMTv.js +1 -0
- package/dist/assets/yaml-BlQTnYKy.js +1 -0
- package/dist/favicon.svg +14 -0
- package/dist/icons.svg +24 -0
- package/dist/index.html +16 -0
- package/package.json +65 -7
- package/server.mjs +3694 -0
package/server.mjs
ADDED
|
@@ -0,0 +1,3694 @@
|
|
|
1
|
+
// server/index.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
|
|
4
|
+
// shared/interactionTypes.ts
|
|
5
|
+
var INTERACTION_TYPES = [
|
|
6
|
+
"form",
|
|
7
|
+
"confirm",
|
|
8
|
+
"input",
|
|
9
|
+
"app_info",
|
|
10
|
+
"api_confirm",
|
|
11
|
+
"api_summary",
|
|
12
|
+
"webhook_confirm",
|
|
13
|
+
"stage_complete",
|
|
14
|
+
"pubkey_input",
|
|
15
|
+
"security_result",
|
|
16
|
+
"webhook_result",
|
|
17
|
+
"webhook_manage",
|
|
18
|
+
"webhook_subscriptions",
|
|
19
|
+
"case_plan",
|
|
20
|
+
"file_changes",
|
|
21
|
+
"git_check",
|
|
22
|
+
"system_action",
|
|
23
|
+
"release_confirm",
|
|
24
|
+
"approval_status",
|
|
25
|
+
"prod_env_info"
|
|
26
|
+
];
|
|
27
|
+
var TYPE_SET = new Set(INTERACTION_TYPES);
|
|
28
|
+
function isInteractionPayload(value) {
|
|
29
|
+
if (!value || typeof value !== "object") return false;
|
|
30
|
+
const type = value.type;
|
|
31
|
+
return typeof type === "string" && TYPE_SET.has(type);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// server/chat/parseOutput.ts
|
|
35
|
+
function summarizeToolInput(name, input) {
|
|
36
|
+
if (!input || typeof input !== "object") return "";
|
|
37
|
+
const first = (keys) => {
|
|
38
|
+
for (const k of keys) {
|
|
39
|
+
const v = input[k];
|
|
40
|
+
if (typeof v === "string" && v) return v;
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
};
|
|
44
|
+
let out = "";
|
|
45
|
+
switch (name) {
|
|
46
|
+
case "Read":
|
|
47
|
+
case "Write":
|
|
48
|
+
case "Edit":
|
|
49
|
+
case "SearchReplace":
|
|
50
|
+
case "NotebookEdit":
|
|
51
|
+
out = first(["file_path", "notebook_path", "path"]);
|
|
52
|
+
break;
|
|
53
|
+
case "Bash":
|
|
54
|
+
out = first(["command"]);
|
|
55
|
+
break;
|
|
56
|
+
case "Glob":
|
|
57
|
+
out = first(["query", "pattern"]);
|
|
58
|
+
break;
|
|
59
|
+
case "Grep":
|
|
60
|
+
out = first(["regex", "pattern"]);
|
|
61
|
+
break;
|
|
62
|
+
case "WebFetch":
|
|
63
|
+
out = first(["url"]);
|
|
64
|
+
break;
|
|
65
|
+
case "WebSearch":
|
|
66
|
+
out = first(["query"]);
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
out = first(["file_path", "path", "command", "query", "url"]);
|
|
70
|
+
}
|
|
71
|
+
return out.length > 200 ? out.slice(0, 200) + "\u2026" : out;
|
|
72
|
+
}
|
|
73
|
+
function extractToolResultText(block) {
|
|
74
|
+
const c = block?.content;
|
|
75
|
+
let out = "";
|
|
76
|
+
if (typeof c === "string") out = c;
|
|
77
|
+
else if (Array.isArray(c)) out = c.map((b) => typeof b === "string" ? b : b?.text || "").filter(Boolean).join("\n");
|
|
78
|
+
out = out.trim();
|
|
79
|
+
return out.length > 300 ? out.slice(0, 300) + "\u2026" : out;
|
|
80
|
+
}
|
|
81
|
+
function extractInteractionBlock(text) {
|
|
82
|
+
const primary = /```interaction\n([\s\S]*?)```/;
|
|
83
|
+
const m = text.match(primary);
|
|
84
|
+
if (m) {
|
|
85
|
+
try {
|
|
86
|
+
return { textBefore: text.substring(0, m.index).trim(), interaction: JSON.parse(m[1].trim()) };
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const fenceRe = /```[a-zA-Z]*\n([\s\S]*?)```/g;
|
|
91
|
+
let fm;
|
|
92
|
+
while (fm = fenceRe.exec(text)) {
|
|
93
|
+
const body = fm[1].trim();
|
|
94
|
+
if (!body.startsWith("{")) continue;
|
|
95
|
+
try {
|
|
96
|
+
const obj = JSON.parse(body);
|
|
97
|
+
if (isInteractionPayload(obj)) {
|
|
98
|
+
return { textBefore: text.substring(0, fm.index).trim(), interaction: obj };
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function isFinalOutput(interaction, textBefore) {
|
|
106
|
+
return interaction.type === "final" || textBefore.includes("\u96C6\u6210\u65B9\u6848") && textBefore.includes("API \u63A5\u53E3\u6E05\u5355");
|
|
107
|
+
}
|
|
108
|
+
function parseAIOutput(rawOutput, sessionId) {
|
|
109
|
+
const lines = rawOutput.trim().split("\n").filter((l) => l.trim());
|
|
110
|
+
let thinking = "";
|
|
111
|
+
let textContent = "";
|
|
112
|
+
let sawStreamJson = false;
|
|
113
|
+
const segments = [];
|
|
114
|
+
const toolSegIdxById = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
try {
|
|
117
|
+
const obj = JSON.parse(line);
|
|
118
|
+
if (typeof obj?.type === "string") sawStreamJson = true;
|
|
119
|
+
if (obj.type === "system") continue;
|
|
120
|
+
if (obj.type === "assistant" && obj.message?.content) {
|
|
121
|
+
for (const block of obj.message.content) {
|
|
122
|
+
if (block.type === "thinking" && block.thinking) {
|
|
123
|
+
thinking += block.thinking;
|
|
124
|
+
segments.push({ type: "thinking", content: block.thinking });
|
|
125
|
+
} else if (block.type === "text" && block.text) {
|
|
126
|
+
textContent += block.text;
|
|
127
|
+
segments.push({ type: "text", content: block.text });
|
|
128
|
+
} else if (block.type === "tool_use" && block.name) {
|
|
129
|
+
const idx = segments.length;
|
|
130
|
+
segments.push({ type: "tool", content: block.name, detail: summarizeToolInput(block.name, block.input) || void 0 });
|
|
131
|
+
if (block.id) toolSegIdxById.set(block.id, idx);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else if (obj.type === "user" && obj.message?.content) {
|
|
135
|
+
for (const block of obj.message.content) {
|
|
136
|
+
if (block.type === "tool_result") {
|
|
137
|
+
const idx = block.tool_use_id ? toolSegIdxById.get(block.tool_use_id) : void 0;
|
|
138
|
+
if (idx !== void 0) {
|
|
139
|
+
const preview = extractToolResultText(block);
|
|
140
|
+
if (preview) segments[idx].result = preview;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else if (obj.type === "result" && obj.result) {
|
|
145
|
+
if (!textContent) textContent = obj.result;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (sawStreamJson && !textContent && !thinking) {
|
|
151
|
+
return { type: "text", text: "", sessionId, error: "empty_output" };
|
|
152
|
+
}
|
|
153
|
+
if (!textContent && !thinking) {
|
|
154
|
+
const output = rawOutput.trim();
|
|
155
|
+
const extracted2 = extractInteractionBlock(output);
|
|
156
|
+
if (extracted2) {
|
|
157
|
+
const type = isFinalOutput(extracted2.interaction, extracted2.textBefore) ? "final" : "interaction";
|
|
158
|
+
return { type, text: extracted2.textBefore, interaction: extracted2.interaction, sessionId };
|
|
159
|
+
}
|
|
160
|
+
return { type: "text", text: output, sessionId };
|
|
161
|
+
}
|
|
162
|
+
const segs = segments.length ? segments : void 0;
|
|
163
|
+
const extracted = extractInteractionBlock(textContent);
|
|
164
|
+
if (extracted) {
|
|
165
|
+
const type = isFinalOutput(extracted.interaction, extracted.textBefore) ? "final" : "interaction";
|
|
166
|
+
return { type, text: extracted.textBefore, thinking: thinking || void 0, interaction: extracted.interaction, sessionId, segments: segs };
|
|
167
|
+
}
|
|
168
|
+
if (textContent.includes("\u96C6\u6210\u65B9\u6848") && textContent.includes("| # |")) {
|
|
169
|
+
return { type: "final", text: textContent, thinking: thinking || void 0, sessionId, segments: segs };
|
|
170
|
+
}
|
|
171
|
+
return { type: "text", text: textContent, thinking: thinking || void 0, sessionId, segments: segs };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// server/chat/skills.ts
|
|
175
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
176
|
+
import { join, dirname } from "node:path";
|
|
177
|
+
import { tmpdir } from "node:os";
|
|
178
|
+
import { randomUUID } from "node:crypto";
|
|
179
|
+
import { fileURLToPath } from "node:url";
|
|
180
|
+
|
|
181
|
+
// shared/steps.ts
|
|
182
|
+
var STEP_IDS = [
|
|
183
|
+
"basic_info",
|
|
184
|
+
"webhook",
|
|
185
|
+
"security_setting",
|
|
186
|
+
"dev_settings",
|
|
187
|
+
"sys_analysis",
|
|
188
|
+
"coding",
|
|
189
|
+
"integration_case",
|
|
190
|
+
"case_execution",
|
|
191
|
+
"release_submit",
|
|
192
|
+
"prod_config"
|
|
193
|
+
];
|
|
194
|
+
var STAGE_GROUP_IDS = ["create", "integrate", "accept", "release"];
|
|
195
|
+
var INTENT_SKILLS = ["wf-api-intent-recognition", "wf-webhook-setting"];
|
|
196
|
+
var STEPS = {
|
|
197
|
+
basic_info: { id: "basic_info", order: 1, group: "create", skills: INTENT_SKILLS, comparable: true },
|
|
198
|
+
webhook: { id: "webhook", order: 2, group: "create", skills: INTENT_SKILLS, comparable: true },
|
|
199
|
+
security_setting: { id: "security_setting", order: 3, group: "create", skills: ["wf-security-setting"], comparable: true },
|
|
200
|
+
dev_settings: { id: "dev_settings", order: 4, group: "integrate", skills: ["wf-dev-setting"], comparable: false },
|
|
201
|
+
sys_analysis: { id: "sys_analysis", order: 5, group: "integrate", skills: ["wf-sys-analysis", "wf-api-integration-upgrade"], comparable: false },
|
|
202
|
+
coding: { id: "coding", order: 6, group: "integrate", skills: ["wf-coding", "wf-api-integration-upgrade"], comparable: false },
|
|
203
|
+
integration_case: { id: "integration_case", order: 7, group: "accept", skills: ["wf-integration-case"], comparable: false },
|
|
204
|
+
case_execution: { id: "case_execution", order: 8, group: "accept", skills: [], comparable: false },
|
|
205
|
+
release_submit: { id: "release_submit", order: 9, group: "release", skills: ["wf-release-submit"], comparable: false },
|
|
206
|
+
prod_config: { id: "prod_config", order: 10, group: "release", skills: ["wf-prod-config"], comparable: false }
|
|
207
|
+
};
|
|
208
|
+
var STEP_ORDER = [...STEP_IDS].sort((a, b) => STEPS[a].order - STEPS[b].order);
|
|
209
|
+
var COMPARABLE_STEPS = STEP_ORDER.filter((id) => STEPS[id].comparable);
|
|
210
|
+
var STAGE_GROUPS = STAGE_GROUP_IDS.map((group) => ({
|
|
211
|
+
group,
|
|
212
|
+
steps: STEP_ORDER.filter((id) => STEPS[id].group === group)
|
|
213
|
+
}));
|
|
214
|
+
function isStepId(value) {
|
|
215
|
+
return typeof value === "string" && value in STEPS;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// server/chat/skills.ts
|
|
219
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
220
|
+
var PROJECT_SKILLS_DIR = join(__dirname, "..", "..", "skills");
|
|
221
|
+
var QODER_SKILLS_DIR = join(process.env.HOME || "", ".qoder", "skills");
|
|
222
|
+
var SKILL_EXTRAS = {
|
|
223
|
+
"wf-api-intent-recognition": { liveCatalog: true },
|
|
224
|
+
"wf-integration-case": {
|
|
225
|
+
dataFile: join("data", "case-scenario-catalog.json"),
|
|
226
|
+
dataLabel: "Case Scenario Catalog Data (case-scenario-catalog.json)"
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
function resolveSkillFile(skillName, relative) {
|
|
230
|
+
for (const base of [PROJECT_SKILLS_DIR, QODER_SKILLS_DIR]) {
|
|
231
|
+
const p = join(base, skillName, relative);
|
|
232
|
+
if (existsSync(p)) return p;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
function skillBody(raw) {
|
|
237
|
+
return raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/)?.[1].trim() ?? raw;
|
|
238
|
+
}
|
|
239
|
+
var LANGUAGE_REQUIREMENT = {
|
|
240
|
+
en: "# Language Requirement\n\nCommunicate entirely in English throughout this conversation. ALL user-visible text \u2014 including interaction card fields (title/description/options/labels) \u2014 must be in English. When a skill instructs you to output a fixed Chinese text verbatim, output the natural English equivalent instead. Protocol fields in interaction JSON (type, options[].value, ids) must remain unchanged.\n\n",
|
|
241
|
+
zh: "# \u8BED\u8A00\u8981\u6C42\n\n\u5168\u7A0B\u4F7F\u7528\u7B80\u4F53\u4E2D\u6587\u4E0E\u7528\u6237\u4EA4\u6D41\uFF08\u542B\u6240\u6709 interaction \u5361\u7247\u7684 title/description/options/label \u7B49\u7528\u6237\u53EF\u89C1\u5B57\u6BB5\uFF09\u3002\n\n"
|
|
242
|
+
};
|
|
243
|
+
var SYS_ANALYSIS_DOC = "docs/wf-system-analysis.md";
|
|
244
|
+
function runtimeBlock(rt) {
|
|
245
|
+
if (!rt) return "";
|
|
246
|
+
const lines = ["# \u8FD0\u884C\u73AF\u5883\uFF08\u8C03\u7528 CLI \u4E0E\u5B9A\u4F4D\u5E94\u7528\u65F6\u5FC5\u987B\u9075\u5FAA\uFF09", ""];
|
|
247
|
+
if (rt.cliPath) {
|
|
248
|
+
lines.push(`- wf-developer CLI \u53EF\u6267\u884C\u6587\u4EF6\u8DEF\u5F84\uFF1A\`${rt.cliPath}\``);
|
|
249
|
+
lines.push(`- \u6280\u80FD\u6587\u6863\u4E2D\u51E1\u51FA\u73B0 \`bin/wf-developer\` \u6216\u88F8 \`wf-developer\` \u547D\u4EE4\uFF0C\u4E00\u5F8B\u7528\u4E0A\u8FF0\u7EDD\u5BF9\u8DEF\u5F84\u66FF\u6362\u540E\u518D\u6267\u884C\uFF08\u4F8B\u5982 Bash \u8C03\u7528\uFF1A\`"${rt.cliPath}" apps overview --app-id <ClientId> --json\`\uFF09\u3002\u8DEF\u5F84\u542B\u7A7A\u683C\u65F6\u8BB0\u5F97\u52A0\u5F15\u53F7\u3002`);
|
|
250
|
+
} else {
|
|
251
|
+
lines.push("- wf-developer CLI \u8DEF\u5F84\uFF1A\u672A\u6307\u5B9A\u3002\u53EF\u5148\u5C1D\u8BD5 PATH \u4E2D\u7684 `wf-developer`\uFF1B\u82E5\u8C03\u7528\u5931\u8D25\uFF0C\u8BF7\u63D0\u793A\u7528\u6237\u5230\u542F\u52A8\u68C0\u67E5\u9875\u6307\u5B9A CLI \u5B89\u88C5\u8DEF\u5F84\uFF0C\u4E25\u7981\u7F16\u9020\u8DEF\u5F84\u3002");
|
|
252
|
+
}
|
|
253
|
+
if (rt.baseUrl) lines.push(`- \u6388\u6743\u7F51\u5173\u5730\u5740\uFF08\u5982\u9700\u663E\u5F0F\u4F20 --base-url\uFF09\uFF1A${rt.baseUrl}`);
|
|
254
|
+
lines.push(
|
|
255
|
+
"- \u65AD\u8A00\u4E0E\u9884\u671F\u63CF\u8FF0\u4E25\u7981\u51FA\u73B0\u5185\u90E8\u7ED3\u679C\u7801\uFF08\u5982 IPAY_RS_* \u7B49 resultCodeId\uFF09\uFF1A\u4E00\u5F8B\u4F7F\u7528\u63A5\u53E3\u6587\u6863\u7684\u7528\u6237\u53EF\u89C1\u6210\u529F\u8BED\u4E49\uFF08resultStatus=S\u3001\u4E1A\u52A1\u5B57\u6BB5/\u72B6\u6001\u503C\u5339\u914D\uFF09\uFF1B\u4E5F\u4E0D\u5F97\u590D\u7528\u4F1A\u8BDD\u5386\u53F2\u4E2D\u7684\u65E7\u65AD\u8A00\u6587\u672C\u3002"
|
|
256
|
+
);
|
|
257
|
+
if (rt.clientId) {
|
|
258
|
+
lines.push(`- \u5F53\u524D\u5E94\u7528 ClientId\uFF1A${rt.clientId}\uFF08\u8C03\u7528 apps/webhooks \u7B49\u547D\u4EE4\u65F6 --app-id \u7528\u5B83\uFF0C\u4E25\u7981\u7F16\u9020\u6216\u81C6\u6D4B\uFF09`);
|
|
259
|
+
} else {
|
|
260
|
+
lines.push("- \u5F53\u524D\u5E94\u7528 ClientId\uFF1A\u5C1A\u672A\u521B\u5EFA\uFF08\u57FA\u7840\u4FE1\u606F\u8282\u70B9\u521B\u5EFA\u6210\u529F\u540E\u624D\u6709\uFF09\u3002\u521B\u5EFA\u524D\u4E0D\u8981\u8C03\u7528\u9700\u8981 --app-id \u7684\u547D\u4EE4\u3002");
|
|
261
|
+
}
|
|
262
|
+
if (rt.appFacts && rt.appFacts.trim()) {
|
|
263
|
+
lines.push("", "# \u5F53\u524D\u5E94\u7528\u670D\u52A1\u7AEF\u4E8B\u5B9E\uFF08\u6253\u5F00\u5E94\u7528\u65F6\u7F13\u5B58\uFF0C\u4EE5\u6B64\u4E3A\u51C6\uFF0C\u52FF\u91CD\u590D\u8C03\u7528 apps overview\uFF09", "", rt.appFacts.trim());
|
|
264
|
+
}
|
|
265
|
+
if (rt.dataUpdated && rt.dataUpdated.trim()) {
|
|
266
|
+
lines.push("", "# \u670D\u52A1\u7AEF\u6570\u636E\u5DF2\u66F4\u65B0\uFF08\u672C\u5730\u8BB0\u5F55\u4E0E\u6700\u65B0\u670D\u52A1\u7AEF\u4E8B\u5B9E\u4E0D\u4E00\u81F4\uFF0C\u9010\u9879\u5DEE\u5F02\u5982\u4E0B\uFF09", "", rt.dataUpdated.trim());
|
|
267
|
+
}
|
|
268
|
+
if (rt.projectDir && rt.projectDir.trim()) {
|
|
269
|
+
lines.push(
|
|
270
|
+
"",
|
|
271
|
+
"# \u5BA2\u6237\u5DE5\u7A0B\u6839\u76EE\u5F55\uFF08\u96C6\u6210\u9636\u6BB5\u4EE3\u7801/\u6587\u6863\u8BFB\u5199\u7684\u7EDD\u5BF9\u8DEF\u5F84\u57FA\u51C6\uFF09",
|
|
272
|
+
"",
|
|
273
|
+
`- \u9879\u76EE\u6839\u76EE\u5F55\uFF1A${rt.projectDir.trim()}`,
|
|
274
|
+
`- \u7CFB\u5206\u8BBE\u8BA1\u6587\u6863\u8DEF\u5F84\uFF1A${rt.projectDir.trim()}/${SYS_ANALYSIS_DOC}`,
|
|
275
|
+
"- \u8BFB\u5199\u4EE3\u7801\u3001\u6267\u884C git \u65F6\u4E00\u5F8B\u4F7F\u7528\u4E0A\u8FF0\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u5982 `git -C <\u9879\u76EE\u6839\u76EE\u5F55> status`\uFF09\uFF1B\u4E25\u7981\u5728\u63A7\u5236\u53F0\u81EA\u8EAB\u76EE\u5F55\u91CC\u64CD\u4F5C\u3002"
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return lines.join("\n") + "\n\n";
|
|
279
|
+
}
|
|
280
|
+
var NODE_LABELS = {
|
|
281
|
+
basic_info: "\u57FA\u7840\u4FE1\u606F",
|
|
282
|
+
webhook: "Webhook \u8BBE\u7F6E",
|
|
283
|
+
security_setting: "\u5B89\u5168\u8BBE\u7F6E",
|
|
284
|
+
dev_settings: "\u5DE5\u7A0B\u8BBE\u7F6E",
|
|
285
|
+
sys_analysis: "\u7CFB\u5206\u8BBE\u8BA1",
|
|
286
|
+
coding: "\u7F16\u7801",
|
|
287
|
+
integration_case: "\u96C6\u6210\u7528\u4F8B\u89C4\u5212",
|
|
288
|
+
case_execution: "\u6267\u884C\u96C6\u6210\u7528\u4F8B",
|
|
289
|
+
release_submit: "\u63D0\u4EA4\u53D1\u5E03",
|
|
290
|
+
prod_config: "\u751F\u4EA7\u914D\u7F6E"
|
|
291
|
+
};
|
|
292
|
+
function nodeGuardBlock(stage) {
|
|
293
|
+
const current = isStepId(stage) ? stage : "basic_info";
|
|
294
|
+
const total = STEP_ORDER.length;
|
|
295
|
+
const currentIdx = STEP_ORDER.indexOf(current);
|
|
296
|
+
const currentLabel = NODE_LABELS[current];
|
|
297
|
+
const list = STEP_ORDER.map((id, i) => {
|
|
298
|
+
const mark = id === current ? " \u2190 \u5F53\u524D\u8282\u70B9" : "";
|
|
299
|
+
return `${i + 1}. ${NODE_LABELS[id]}${mark}`;
|
|
300
|
+
}).join("\n");
|
|
301
|
+
return [
|
|
302
|
+
"# \u8282\u70B9\u8FB9\u754C\u5B88\u536B\uFF08\u94C1\u5F8B\uFF0C\u4F18\u5148\u7EA7\u9AD8\u4E8E\u4E0B\u65B9\u6240\u6709 skill \u6D41\u7A0B\uFF09",
|
|
303
|
+
"",
|
|
304
|
+
`\u4F60\u5F53\u524D\u53EA\u8D1F\u8D23\u3010${currentLabel}\u3011\u8282\u70B9\uFF08\u7B2C ${currentIdx + 1} \u6B65 / \u5171 ${total} \u6B65\uFF09\u3002\u96C6\u6210\u6D41\u6C34\u7EBF\u7684\u8282\u70B9\u987A\u5E8F\u5982\u4E0B\uFF1A`,
|
|
305
|
+
"",
|
|
306
|
+
list,
|
|
307
|
+
"",
|
|
308
|
+
"## \u4E00\u3001\u53EA\u505A\u672C\u8282\u70B9\u7684\u4E8B",
|
|
309
|
+
"",
|
|
310
|
+
"\u5F53\u7528\u6237\u5728\u672C\u8282\u70B9\u7684\u5BF9\u8BDD\u91CC\u8981\u6C42\u53BB\u505A**\u5176\u5B83\u8282\u70B9**\u7684\u4E8B\u60C5\u65F6\uFF0C\u4F60\u5FC5\u987B\u5148\u963B\u6B62\u3001\u4E0D\u5F97\u76F4\u63A5\u6267\u884C\uFF0C\u5E76\u6309\u4EE5\u4E0B\u60C5\u51B5\u5F15\u5BFC\uFF08\u5F15\u5BFC\u7528\u4E00\u6BB5\u81EA\u7136\u3001\u7B80\u77ED\u7684\u6587\u672C\u8BF4\u660E\uFF0C\u9664\u975E\u662F\u7B2C\u4E8C\u8282\u8981\u6C42\u8F93\u51FA\u7684\u63A8\u8FDB\u5361\u7247\uFF0C\u5426\u5219\u4E0D\u8981\u8F93\u51FA interaction \u5361\u7247\uFF09\uFF1A",
|
|
311
|
+
"",
|
|
312
|
+
`A. \u7528\u6237\u60F3\u505A**\u6392\u5728\u3010${currentLabel}\u3011\u4E4B\u540E**\u7684\u8282\u70B9\uFF08\u4F8B\uFF1A\u4F60\u5728\u7CFB\u5206\u8BBE\u8BA1\u8282\u70B9\uFF0C\u7528\u6237\u8BF4\u201C\u6211\u8981\u7F16\u7801\u201D\uFF09\uFF1A`,
|
|
313
|
+
` - \u82E5\u3010${currentLabel}\u3011\u7684\u5DE5\u4F5C**\u5C1A\u672A\u5B8C\u6210**\uFF1A\u53EA\u9700\u544A\u8BC9\u7528\u6237\u300C\u8BF7\u5148\u5B8C\u6210\u5F53\u524D\u7684\u300E${currentLabel}\u300F\u9636\u6BB5\u300D\uFF0C\u5E76\u7EE7\u7EED\u63A8\u8FDB\u672C\u8282\u70B9\uFF0C\u5207\u52FF\u5F00\u59CB\u540E\u7EED\u8282\u70B9\u7684\u5DE5\u4F5C\u3002`,
|
|
314
|
+
` - \u82E5\u3010${currentLabel}\u3011\u7684\u5DE5\u4F5C**\u5DF2\u7ECF\u5B8C\u6210**\uFF08\u672C\u8F6E\u6216\u6B64\u524D\u5DF2\u4EA7\u51FA\u672C\u8282\u70B9\u5E94\u4EA4\u4ED8\u7684\u4EA7\u7269 / \u5DF2\u5177\u5907\u63A8\u8FDB\u6761\u4EF6\uFF09\uFF1A**\u4E0D\u8981\u7528\u7EAF\u6587\u5B57\u8BE2\u95EE\u300C\u662F\u5426\u63A8\u8FDB\u300D\uFF0C\u4E5F\u4E0D\u8981\u76F4\u63A5\u8DF3\u5230\u76EE\u6807\u8282\u70B9\u6267\u884C**\uFF0C\u800C\u662F\u6309\u4E0B\u65B9\u300C\u4E8C\u3001\u63A8\u8FDB\u5FC5\u987B\u7528\u5361\u7247\u300D\u8F93\u51FA\u672C\u8282\u70B9\u7684\u300C\u8FDB\u5165\u4E0B\u4E00\u9636\u6BB5\u300D\u63A8\u8FDB\u5361\u7247\uFF0C\u7531\u7528\u6237\u70B9\u51FB\u5361\u7247\u540E\u624D\u63A8\u8FDB\u5230\u4E0B\u4E00\u8282\u70B9\uFF1B\u7528\u6237\u70B9\u51FB\u4E4B\u524D\u4E0D\u5F97\u6267\u884C\u76EE\u6807\u8282\u70B9\u7684\u5DE5\u4F5C\u3002`,
|
|
315
|
+
"",
|
|
316
|
+
`B. \u7528\u6237\u60F3\u505A**\u6392\u5728\u3010${currentLabel}\u3011\u4E4B\u524D**\u3001\u901A\u5E38\u5DF2\u5B8C\u6210\u8FC7\u7684\u8282\u70B9\uFF08\u4F8B\uFF1A\u5B89\u5168\u8BBE\u7F6E\u3001Webhook \u8BBE\u7F6E\u7B49\uFF09\uFF1A\u544A\u8BC9\u7528\u6237\u300C\u8BE5\u4E8B\u9879\u5C5E\u4E8E\u300E\uFF08\u5BF9\u5E94\u8282\u70B9\u540D\uFF09\u300F\u8282\u70B9\uFF0C\u8BF7\u56DE\u5230\u5DE6\u4FA7\u6D41\u6C34\u7EBF\u7684\u300E\uFF08\u5BF9\u5E94\u8282\u70B9\u540D\uFF09\u300F\u8282\u70B9\u53BB\u5B8C\u6210\u300D\uFF0C\u4E0D\u8981\u5728\u672C\u8282\u70B9\u4EE3\u505A\u3002`,
|
|
317
|
+
"",
|
|
318
|
+
"C. \u65E0\u6CD5\u5224\u65AD\u7528\u6237\u60F3\u505A\u54EA\u4E2A\u8282\u70B9\u3001\u6216\u7528\u6237\u53EA\u662F\u666E\u901A\u63D0\u95EE / \u9700\u6C42\u672C\u5C31\u4E0E\u672C\u8282\u70B9\u76F8\u5173\uFF1A\u6B63\u5E38\u6309\u4E0B\u65B9 skill \u6D41\u7A0B\u5904\u7406\uFF0C\u4E0D\u8981\u8BEF\u62E6\u3002",
|
|
319
|
+
"",
|
|
320
|
+
"## \u4E8C\u3001\u63A8\u8FDB\u5FC5\u987B\u7528\u5361\u7247\uFF08\u94C1\u5F8B\uFF09",
|
|
321
|
+
"",
|
|
322
|
+
`\u53EA\u8981\u3010${currentLabel}\u3011\u7684\u5DE5\u4F5C\u5DF2\u5B8C\u6210\u3001\u53EF\u4EE5\u8FDB\u5165\u4E0B\u4E00\u8282\u70B9\u2014\u2014\u65E0\u8BBA\u662F\u7528\u6237\u4E3B\u52A8\u8981\u6C42\u8FDB\u5165\u4E0B\u4E00\u6B65\uFF0C\u8FD8\u662F\u672C\u8282\u70B9\u6D41\u7A0B\u81EA\u7136\u8D70\u5230\u6536\u5C3E\u2014\u2014\u4F60**\u5FC5\u987B\u8F93\u51FA\u672C\u8282\u70B9 skill \u6D41\u7A0B\u4E2D\u5B9A\u4E49\u7684\u300C\u8FDB\u5165\u4E0B\u4E00\u9636\u6BB5\u300D\u63A8\u8FDB\u5361\u7247**\uFF0C\u5373\u5E26\u300C\u4E0B\u4E00\u6B65 / \u8FDB\u5165\u4E0B\u4E00\u9636\u6BB5 / \u5B8C\u6210\u300D\u52A8\u4F5C\uFF08action \u4E3A next \u6216 finish\uFF09\u7684 interaction \u5361\u7247\uFF08\u5982 stage_complete / security_result / webhook_result / app_info / webhook_manage \u7B49\uFF0C\u5177\u4F53\u4EE5\u672C\u8282\u70B9 skill \u5B9A\u4E49\u4E3A\u51C6\uFF09\u3002`,
|
|
323
|
+
"",
|
|
324
|
+
"- **\u4E25\u7981\u53EA\u7528\u6587\u5B57\u8BF4\u300C\u5DF2\u5B8C\u6210\uFF0C\u53EF\u4EE5\u8FDB\u5165\u4E0B\u4E00\u6B65 / \u4F60\u53EF\u4EE5\u8FDB\u5165\u4E0B\u4E00\u4E2A\u8282\u70B9\u4E86\u300D\u800C\u4E0D\u7ED9\u51FA\u8BE5\u63A8\u8FDB\u5361\u7247**\uFF1B\u7528\u6237\u53EA\u6709\u70B9\u51FB\u8BE5\u5361\u7247\u624D\u80FD\u63A8\u8FDB\u5230\u4E0B\u4E00\u8282\u70B9\u3002",
|
|
325
|
+
"- \u5361\u7247\u524D\u53EF\u9644\u4E00\u53E5\u7B80\u77ED\u8BF4\u660E\uFF0C\u4F46\u63A8\u8FDB\u52A8\u4F5C\u53EA\u80FD\u7531\u5361\u7247\u627F\u8F7D\uFF0C\u4E0D\u5F97\u7528\u7EAF\u6587\u5B57\u66FF\u4EE3\u3002",
|
|
326
|
+
"",
|
|
327
|
+
`\u5224\u65AD\u300C\u3010${currentLabel}\u3011\u662F\u5426\u5DF2\u5B8C\u6210\u300D\u4EE5\u672C\u8282\u70B9 skill \u5B9A\u4E49\u7684\u5B8C\u6210\u6807\u5FD7\u4E3A\u51C6\uFF08\u5982\u5DF2\u4EA7\u51FA\u8BE5\u8282\u70B9\u5E94\u4EA4\u4ED8\u7684\u4EA7\u7269\u3001\u5DF2\u5177\u5907\u8F93\u51FA\u4E0A\u8FF0\u63A8\u8FDB\u5361\u7247\u7684\u6761\u4EF6\uFF09\u3002`,
|
|
328
|
+
""
|
|
329
|
+
].join("\n") + "\n";
|
|
330
|
+
}
|
|
331
|
+
async function createSkillPromptFile(stage, lang, fetchLiveCatalog, runtime) {
|
|
332
|
+
const skills = STEPS[isStepId(stage) ? stage : "basic_info"].skills;
|
|
333
|
+
let prompt = runtimeBlock(runtime) + nodeGuardBlock(stage);
|
|
334
|
+
for (const name of skills) {
|
|
335
|
+
const skillPath = resolveSkillFile(name, "SKILL.md");
|
|
336
|
+
if (skillPath) prompt += `# Skill: ${name}
|
|
337
|
+
|
|
338
|
+
${skillBody(readFileSync(skillPath, "utf-8"))}
|
|
339
|
+
|
|
340
|
+
`;
|
|
341
|
+
const extra = SKILL_EXTRAS[name];
|
|
342
|
+
if (!extra) continue;
|
|
343
|
+
if (extra.liveCatalog) {
|
|
344
|
+
const live = await fetchLiveCatalog();
|
|
345
|
+
if (live.ok) prompt += `# Live API Catalog (wf-developer apps catalog \u5B9E\u65F6\u63A5\u53E3\u76EE\u5F55)
|
|
346
|
+
|
|
347
|
+
${JSON.stringify(live.data, null, 2)}
|
|
348
|
+
|
|
349
|
+
`;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (extra.dataFile) {
|
|
353
|
+
const dataPath = resolveSkillFile(name, extra.dataFile);
|
|
354
|
+
const content = dataPath ? readFileSync(dataPath, "utf-8") : "";
|
|
355
|
+
if (content) prompt += `# ${extra.dataLabel}
|
|
356
|
+
|
|
357
|
+
${content}
|
|
358
|
+
|
|
359
|
+
`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
prompt += lang === "en" ? LANGUAGE_REQUIREMENT.en : LANGUAGE_REQUIREMENT.zh;
|
|
363
|
+
const projectDir = runtime?.projectDir?.trim();
|
|
364
|
+
const docPath = projectDir ? `${projectDir}/${SYS_ANALYSIS_DOC}` : SYS_ANALYSIS_DOC;
|
|
365
|
+
prompt = prompt.replace(/\{\{\s*projectDir\s*\}\}/g, projectDir || "\uFF08\u5BA2\u6237\u5DE5\u7A0B\u6839\u76EE\u5F55\uFF0C\u4EE5\u7528\u6237\u5728\u5DE5\u7A0B\u8BBE\u7F6E\u8282\u70B9\u63D0\u4F9B\u7684\u76EE\u5F55\u4E3A\u51C6\uFF09").replace(/\{\{\s*docPath\s*\}\}/g, docPath);
|
|
366
|
+
const tempFile = join(tmpdir(), `qoder-skill-${randomUUID()}.md`);
|
|
367
|
+
writeFileSync(tempFile, prompt, "utf-8");
|
|
368
|
+
return tempFile;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// server/cli/wfDeveloper.ts
|
|
372
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
373
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2, unlinkSync } from "node:fs";
|
|
374
|
+
import { join as join3, dirname as dirname3 } from "node:path";
|
|
375
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
376
|
+
|
|
377
|
+
// server/store.ts
|
|
378
|
+
import { DatabaseSync } from "node:sqlite";
|
|
379
|
+
|
|
380
|
+
// shared/keyData.ts
|
|
381
|
+
var FLOW_NODES = STEP_IDS;
|
|
382
|
+
function overviewKeyData(overview) {
|
|
383
|
+
const info = overview?.appInfo;
|
|
384
|
+
const interfaces = (overview?.integratedInterfaces || []).map((i) => ({ interfaceId: Number(i.interfaceId), funcName: i.funcName || "" })).sort((a, b) => a.interfaceId - b.interfaceId);
|
|
385
|
+
const configs = (overview?.webhookConfigs || []).map((c) => ({
|
|
386
|
+
id: Number(c.id),
|
|
387
|
+
name: c.name || "",
|
|
388
|
+
url: c.url || "",
|
|
389
|
+
events: [...(c.eventGroups || []).flatMap((g) => (g.events || []).map((e) => e.eventCode || ""))].sort()
|
|
390
|
+
})).sort((a, b) => a.id - b.id);
|
|
391
|
+
const partner = (info?.publicKeyInfoList || []).find((k) => k.issuer === "partner");
|
|
392
|
+
return {
|
|
393
|
+
basic_info: {
|
|
394
|
+
appName: info?.name || info?.appName || "",
|
|
395
|
+
appState: info?.state || info?.status || "",
|
|
396
|
+
interfaces
|
|
397
|
+
},
|
|
398
|
+
webhook: { configs },
|
|
399
|
+
security_setting: {
|
|
400
|
+
partnerKeySet: !!partner,
|
|
401
|
+
partnerKeyModified: partner?.gmtModified ? String(partner.gmtModified) : ""
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
var LIVE_STATES = ["LIVE", "LIVE_EDITING", "EDIT_TESTING", "EDIT_GO_LIVE_SUBMITTED", "ONLINE", "RELEASED"];
|
|
406
|
+
var PASSED_CASE_STATES = ["PASSED", "PASS", "SUCCESS"];
|
|
407
|
+
function caseStatus(c) {
|
|
408
|
+
return String(c?.status || c?.caseStatus || c?.state || "").toUpperCase();
|
|
409
|
+
}
|
|
410
|
+
function deriveInitialNodes(appStateRaw, overview, cases) {
|
|
411
|
+
const state = (appStateRaw || "").toUpperCase();
|
|
412
|
+
const S = {};
|
|
413
|
+
const done = (...ns) => ns.forEach((n) => S[n] = "DONE");
|
|
414
|
+
done("basic_info");
|
|
415
|
+
if (LIVE_STATES.includes(state)) {
|
|
416
|
+
for (const n of FLOW_NODES) S[n] = "DONE";
|
|
417
|
+
return { statuses: S, current: "prod_config" };
|
|
418
|
+
}
|
|
419
|
+
const wh = overviewKeyData(overview).webhook?.configs || [];
|
|
420
|
+
if (state === "GO_LIVE_SUBMITTED") {
|
|
421
|
+
done("webhook", "security_setting", "dev_settings", "sys_analysis", "coding", "integration_case", "case_execution");
|
|
422
|
+
S["release_submit"] = "IN_PROGRESS";
|
|
423
|
+
return { statuses: S, current: "release_submit" };
|
|
424
|
+
}
|
|
425
|
+
if (state === "TESTING") {
|
|
426
|
+
done("webhook", "security_setting");
|
|
427
|
+
const list = Array.isArray(cases) ? cases : [];
|
|
428
|
+
if (list.length === 0) {
|
|
429
|
+
S["dev_settings"] = "IN_PROGRESS";
|
|
430
|
+
return { statuses: S, current: "dev_settings" };
|
|
431
|
+
}
|
|
432
|
+
done("dev_settings", "sys_analysis", "coding", "integration_case");
|
|
433
|
+
const allPassed = list.every((c) => PASSED_CASE_STATES.includes(caseStatus(c)));
|
|
434
|
+
if (!allPassed) {
|
|
435
|
+
S["case_execution"] = "IN_PROGRESS";
|
|
436
|
+
return { statuses: S, current: "case_execution" };
|
|
437
|
+
}
|
|
438
|
+
done("case_execution");
|
|
439
|
+
S["release_submit"] = "IN_PROGRESS";
|
|
440
|
+
return { statuses: S, current: "release_submit" };
|
|
441
|
+
}
|
|
442
|
+
if (wh.length === 0) {
|
|
443
|
+
S["webhook"] = "IN_PROGRESS";
|
|
444
|
+
return { statuses: S, current: "webhook" };
|
|
445
|
+
}
|
|
446
|
+
done("webhook");
|
|
447
|
+
S["security_setting"] = "IN_PROGRESS";
|
|
448
|
+
return { statuses: S, current: "security_setting" };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// server/store.ts
|
|
452
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
453
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
454
|
+
import { mkdirSync } from "node:fs";
|
|
455
|
+
var now = () => Date.now();
|
|
456
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
457
|
+
var __dirname2 = dirname2(__filename);
|
|
458
|
+
var DATA_DIR = join2(__dirname2, "..", "data");
|
|
459
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
460
|
+
var db = new DatabaseSync(join2(DATA_DIR, "wf-developer-console.db"));
|
|
461
|
+
db.exec(`
|
|
462
|
+
CREATE TABLE IF NOT EXISTS dc_users (
|
|
463
|
+
user_id TEXT PRIMARY KEY,
|
|
464
|
+
email TEXT DEFAULT '',
|
|
465
|
+
active_client_id TEXT DEFAULT '',
|
|
466
|
+
created_at INTEGER NOT NULL,
|
|
467
|
+
updated_at INTEGER NOT NULL
|
|
468
|
+
);
|
|
469
|
+
CREATE TABLE IF NOT EXISTS dc_apps (
|
|
470
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
471
|
+
user_id TEXT NOT NULL,
|
|
472
|
+
client_id TEXT NOT NULL,
|
|
473
|
+
app_name TEXT DEFAULT '',
|
|
474
|
+
app_state TEXT DEFAULT '',
|
|
475
|
+
ui_state TEXT DEFAULT '{}',
|
|
476
|
+
overview_snapshot TEXT DEFAULT '',
|
|
477
|
+
overview_synced_at INTEGER DEFAULT 0,
|
|
478
|
+
created_at INTEGER NOT NULL,
|
|
479
|
+
updated_at INTEGER NOT NULL,
|
|
480
|
+
UNIQUE(user_id, client_id)
|
|
481
|
+
);
|
|
482
|
+
CREATE TABLE IF NOT EXISTS dc_pipeline_nodes (
|
|
483
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
484
|
+
user_id TEXT NOT NULL,
|
|
485
|
+
client_id TEXT NOT NULL,
|
|
486
|
+
flow_node TEXT NOT NULL,
|
|
487
|
+
status TEXT NOT NULL DEFAULT 'INIT',
|
|
488
|
+
session_id TEXT DEFAULT '',
|
|
489
|
+
key_data TEXT DEFAULT '{}',
|
|
490
|
+
created_at INTEGER NOT NULL,
|
|
491
|
+
updated_at INTEGER NOT NULL,
|
|
492
|
+
UNIQUE(user_id, client_id, flow_node)
|
|
493
|
+
);
|
|
494
|
+
CREATE TABLE IF NOT EXISTS dc_node_messages (
|
|
495
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
496
|
+
user_id TEXT NOT NULL,
|
|
497
|
+
client_id TEXT NOT NULL,
|
|
498
|
+
flow_node TEXT NOT NULL,
|
|
499
|
+
item_id TEXT NOT NULL,
|
|
500
|
+
seq INTEGER NOT NULL,
|
|
501
|
+
payload TEXT NOT NULL,
|
|
502
|
+
created_at INTEGER NOT NULL,
|
|
503
|
+
UNIQUE(user_id, client_id, flow_node, item_id)
|
|
504
|
+
);
|
|
505
|
+
CREATE TABLE IF NOT EXISTS dc_settings (
|
|
506
|
+
key TEXT PRIMARY KEY,
|
|
507
|
+
value TEXT NOT NULL DEFAULT '',
|
|
508
|
+
updated_at INTEGER NOT NULL
|
|
509
|
+
);
|
|
510
|
+
CREATE INDEX IF NOT EXISTS idx_msgs_scope ON dc_node_messages(user_id, client_id, flow_node);
|
|
511
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_scope ON dc_pipeline_nodes(user_id, client_id);
|
|
512
|
+
`);
|
|
513
|
+
function ensureColumn(table, column, ddl) {
|
|
514
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
515
|
+
if (!cols.some((c) => c.name === column)) {
|
|
516
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function migrateSettingsTable() {
|
|
520
|
+
const cols = db.prepare("PRAGMA table_info(dc_settings)").all();
|
|
521
|
+
const names = cols.map((c) => c.name);
|
|
522
|
+
if (names.includes("key") && names.includes("value")) return;
|
|
523
|
+
if (cols.length === 0) {
|
|
524
|
+
db.exec("CREATE TABLE dc_settings (key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL);");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const ts = now();
|
|
528
|
+
const oldRow = db.prepare("SELECT * FROM dc_settings WHERE id = 1").get();
|
|
529
|
+
db.exec("BEGIN");
|
|
530
|
+
try {
|
|
531
|
+
db.exec("ALTER TABLE dc_settings RENAME TO dc_settings_legacy");
|
|
532
|
+
db.exec("CREATE TABLE dc_settings (key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL);");
|
|
533
|
+
if (oldRow) {
|
|
534
|
+
const ins = db.prepare("INSERT OR IGNORE INTO dc_settings (key, value, updated_at) VALUES (?, ?, ?)");
|
|
535
|
+
for (const name of names) {
|
|
536
|
+
if (name === "id" || name === "updated_at") continue;
|
|
537
|
+
const v = oldRow[name];
|
|
538
|
+
if (v === null || v === void 0) continue;
|
|
539
|
+
ins.run(name, String(v), ts);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
db.exec("DROP TABLE dc_settings_legacy");
|
|
543
|
+
db.exec("COMMIT");
|
|
544
|
+
} catch (e) {
|
|
545
|
+
db.exec("ROLLBACK");
|
|
546
|
+
throw e;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
ensureColumn("dc_apps", "current_node", "TEXT DEFAULT ''");
|
|
550
|
+
ensureColumn("dc_apps", "executor", "TEXT DEFAULT ''");
|
|
551
|
+
ensureColumn("dc_apps", "project_dir", "TEXT DEFAULT ''");
|
|
552
|
+
migrateSettingsTable();
|
|
553
|
+
for (const droppedCol of ["server_changed", "server_change_desc", "baseline_at", "key_data_hash"]) {
|
|
554
|
+
try {
|
|
555
|
+
const nodeCols = db.prepare("PRAGMA table_info(dc_pipeline_nodes)").all();
|
|
556
|
+
if (nodeCols.some((c) => c.name === droppedCol)) db.exec(`ALTER TABLE dc_pipeline_nodes DROP COLUMN ${droppedCol}`);
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const appCols = db.prepare("PRAGMA table_info(dc_apps)").all();
|
|
562
|
+
if (appCols.some((c) => c.name === "app_context")) db.exec("ALTER TABLE dc_apps DROP COLUMN app_context");
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
function mapNodeRow(r) {
|
|
566
|
+
let keyData = {};
|
|
567
|
+
try {
|
|
568
|
+
keyData = JSON.parse(r.key_data || "{}");
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
flowNode: r.flow_node,
|
|
573
|
+
status: r.status,
|
|
574
|
+
sessionId: r.session_id,
|
|
575
|
+
keyData
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function mapAppRow(r) {
|
|
579
|
+
let uiState = {};
|
|
580
|
+
try {
|
|
581
|
+
uiState = JSON.parse(r.ui_state || "{}");
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
clientId: r.client_id,
|
|
586
|
+
appName: r.app_name || "",
|
|
587
|
+
appState: r.app_state || "",
|
|
588
|
+
uiState,
|
|
589
|
+
currentNode: r.current_node || "",
|
|
590
|
+
executor: r.executor || "",
|
|
591
|
+
projectDir: r.project_dir || "",
|
|
592
|
+
overviewSyncedAt: r.overview_synced_at || 0,
|
|
593
|
+
updatedAt: r.updated_at || 0
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function getUser(uid) {
|
|
597
|
+
const u = db.prepare("SELECT * FROM dc_users WHERE user_id = ?").get(uid);
|
|
598
|
+
const apps = db.prepare("SELECT client_id, app_name, current_node, updated_at FROM dc_apps WHERE user_id = ? ORDER BY updated_at DESC").all(uid);
|
|
599
|
+
return {
|
|
600
|
+
serverUserId: uid,
|
|
601
|
+
email: u?.email || "",
|
|
602
|
+
activeClientId: u?.active_client_id || "",
|
|
603
|
+
apps: apps.map((a) => ({ clientId: a.client_id, appName: a.app_name || "", currentNode: a.current_node || "", executor: a.executor || "", updatedAt: a.updated_at || 0 }))
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function upsertUser(uid, opts) {
|
|
607
|
+
const ts = now();
|
|
608
|
+
const existing = db.prepare("SELECT * FROM dc_users WHERE user_id = ?").get(uid);
|
|
609
|
+
if (!existing) {
|
|
610
|
+
db.prepare("INSERT INTO dc_users (user_id, email, active_client_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(uid, opts.email || "", opts.activeClientId || "", ts, ts);
|
|
611
|
+
} else {
|
|
612
|
+
const email = opts.email !== void 0 ? opts.email : existing.email;
|
|
613
|
+
const active = opts.activeClientId !== void 0 ? opts.activeClientId : existing.active_client_id;
|
|
614
|
+
db.prepare("UPDATE dc_users SET email = ?, active_client_id = ?, updated_at = ? WHERE user_id = ?").run(email, active, ts, uid);
|
|
615
|
+
}
|
|
616
|
+
return getUser(uid);
|
|
617
|
+
}
|
|
618
|
+
function getApp(uid, clientId) {
|
|
619
|
+
const a = db.prepare("SELECT * FROM dc_apps WHERE user_id = ? AND client_id = ?").get(uid, clientId);
|
|
620
|
+
if (!a) return null;
|
|
621
|
+
return mapAppRow(a);
|
|
622
|
+
}
|
|
623
|
+
function touchApp(uid, clientId) {
|
|
624
|
+
db.prepare("UPDATE dc_apps SET updated_at = ? WHERE user_id = ? AND client_id = ?").run(now(), uid, clientId);
|
|
625
|
+
}
|
|
626
|
+
function upsertApp(uid, clientId, fields) {
|
|
627
|
+
const ts = now();
|
|
628
|
+
const existing = db.prepare("SELECT * FROM dc_apps WHERE user_id = ? AND client_id = ?").get(uid, clientId);
|
|
629
|
+
if (!existing) {
|
|
630
|
+
db.prepare("INSERT INTO dc_apps (user_id, client_id, app_name, app_state, ui_state, overview_snapshot, overview_synced_at, executor, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)").run(uid, clientId, fields.appName || "", fields.appState || "", JSON.stringify(fields.uiState || {}), fields.overviewSnapshot ? JSON.stringify(fields.overviewSnapshot) : "", fields.executor || "", ts, ts);
|
|
631
|
+
} else {
|
|
632
|
+
db.prepare("UPDATE dc_apps SET app_name = ?, app_state = ?, ui_state = ?, updated_at = ? WHERE user_id = ? AND client_id = ?").run(
|
|
633
|
+
fields.appName !== void 0 ? fields.appName : existing.app_name,
|
|
634
|
+
fields.appState !== void 0 ? fields.appState : existing.app_state,
|
|
635
|
+
fields.uiState !== void 0 ? JSON.stringify(fields.uiState) : existing.ui_state,
|
|
636
|
+
ts,
|
|
637
|
+
uid,
|
|
638
|
+
clientId
|
|
639
|
+
);
|
|
640
|
+
if (fields.executor !== void 0) {
|
|
641
|
+
db.prepare("UPDATE dc_apps SET executor = ?, updated_at = ? WHERE user_id = ? AND client_id = ?").run(fields.executor, ts, uid, clientId);
|
|
642
|
+
}
|
|
643
|
+
if (fields.overviewSnapshot !== void 0) {
|
|
644
|
+
db.prepare("UPDATE dc_apps SET overview_snapshot = ?, overview_synced_at = ? WHERE user_id = ? AND client_id = ?").run(JSON.stringify(fields.overviewSnapshot), ts, uid, clientId);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return getApp(uid, clientId);
|
|
648
|
+
}
|
|
649
|
+
function setAppExecutor(uid, clientId, executor) {
|
|
650
|
+
upsertApp(uid, clientId, { executor });
|
|
651
|
+
return getApp(uid, clientId);
|
|
652
|
+
}
|
|
653
|
+
function setAppProjectDir(uid, clientId, projectDir) {
|
|
654
|
+
const existing = db.prepare("SELECT id FROM dc_apps WHERE user_id = ? AND client_id = ?").get(uid, clientId);
|
|
655
|
+
if (!existing) return;
|
|
656
|
+
db.prepare("UPDATE dc_apps SET project_dir = ?, updated_at = ? WHERE user_id = ? AND client_id = ?").run(projectDir || "", now(), uid, clientId);
|
|
657
|
+
}
|
|
658
|
+
function setUiState(uid, clientId, uiState) {
|
|
659
|
+
const existing = db.prepare("SELECT id FROM dc_apps WHERE user_id = ? AND client_id = ?").get(uid, clientId);
|
|
660
|
+
if (!existing) return;
|
|
661
|
+
db.prepare("UPDATE dc_apps SET ui_state = ?, updated_at = ? WHERE user_id = ? AND client_id = ?").run(JSON.stringify(uiState || {}), now(), uid, clientId);
|
|
662
|
+
}
|
|
663
|
+
function setCurrentNode(uid, clientId, node) {
|
|
664
|
+
const existing = db.prepare("SELECT id FROM dc_apps WHERE user_id = ? AND client_id = ?").get(uid, clientId);
|
|
665
|
+
if (!existing) return;
|
|
666
|
+
db.prepare("UPDATE dc_apps SET current_node = ?, updated_at = ? WHERE user_id = ? AND client_id = ?").run(node || "", now(), uid, clientId);
|
|
667
|
+
}
|
|
668
|
+
function deleteApp(uid, clientId) {
|
|
669
|
+
db.prepare("DELETE FROM dc_node_messages WHERE user_id = ? AND client_id = ?").run(uid, clientId);
|
|
670
|
+
db.prepare("DELETE FROM dc_pipeline_nodes WHERE user_id = ? AND client_id = ?").run(uid, clientId);
|
|
671
|
+
db.prepare("DELETE FROM dc_apps WHERE user_id = ? AND client_id = ?").run(uid, clientId);
|
|
672
|
+
const u = db.prepare("SELECT active_client_id FROM dc_users WHERE user_id = ?").get(uid);
|
|
673
|
+
if (u?.active_client_id === clientId) {
|
|
674
|
+
db.prepare("UPDATE dc_users SET active_client_id = '', updated_at = ? WHERE user_id = ?").run(now(), uid);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function ensureNodes(uid, clientId) {
|
|
678
|
+
const ts = now();
|
|
679
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO dc_pipeline_nodes (user_id, client_id, flow_node, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)");
|
|
680
|
+
for (const node of FLOW_NODES) stmt.run(uid, clientId, node, "INIT", ts, ts);
|
|
681
|
+
}
|
|
682
|
+
function getNodes(uid, clientId) {
|
|
683
|
+
const rows = db.prepare("SELECT * FROM dc_pipeline_nodes WHERE user_id = ? AND client_id = ?").all(uid, clientId);
|
|
684
|
+
const counts = db.prepare(
|
|
685
|
+
"SELECT flow_node, COUNT(*) AS c FROM dc_node_messages WHERE user_id = ? AND client_id = ? GROUP BY flow_node"
|
|
686
|
+
).all(uid, clientId);
|
|
687
|
+
const countMap = new Map(counts.map((c) => [c.flow_node, c.c]));
|
|
688
|
+
return rows.map((r) => ({ ...mapNodeRow(r), messageCount: countMap.get(r.flow_node) || 0 }));
|
|
689
|
+
}
|
|
690
|
+
function getNode(uid, clientId, node) {
|
|
691
|
+
const r = db.prepare("SELECT * FROM dc_pipeline_nodes WHERE user_id = ? AND client_id = ? AND flow_node = ?").get(uid, clientId, node);
|
|
692
|
+
return r ? mapNodeRow(r) : null;
|
|
693
|
+
}
|
|
694
|
+
function updateNode(uid, clientId, node, patch) {
|
|
695
|
+
const ts = now();
|
|
696
|
+
const existing = db.prepare("SELECT * FROM dc_pipeline_nodes WHERE user_id = ? AND client_id = ? AND flow_node = ?").get(uid, clientId, node);
|
|
697
|
+
if (!existing) return;
|
|
698
|
+
let keyData = existing.key_data;
|
|
699
|
+
if (patch.keyDataReplace !== void 0) {
|
|
700
|
+
keyData = JSON.stringify(patch.keyDataReplace);
|
|
701
|
+
} else if (patch.keyDataPatch) {
|
|
702
|
+
let parsed = {};
|
|
703
|
+
try {
|
|
704
|
+
parsed = JSON.parse(existing.key_data || "{}");
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
const merged = { ...parsed, ...patch.keyDataPatch };
|
|
708
|
+
keyData = JSON.stringify(merged);
|
|
709
|
+
}
|
|
710
|
+
db.prepare(`UPDATE dc_pipeline_nodes SET
|
|
711
|
+
status = ?, session_id = ?, key_data = ?, updated_at = ?
|
|
712
|
+
WHERE user_id = ? AND client_id = ? AND flow_node = ?`).run(
|
|
713
|
+
patch.status !== void 0 ? patch.status : existing.status,
|
|
714
|
+
patch.sessionId !== void 0 ? patch.sessionId : existing.session_id,
|
|
715
|
+
keyData,
|
|
716
|
+
ts,
|
|
717
|
+
uid,
|
|
718
|
+
clientId,
|
|
719
|
+
node
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
function setNodesStatus(uid, clientId, statusMap) {
|
|
723
|
+
for (const [node, status] of Object.entries(statusMap)) {
|
|
724
|
+
if (!FLOW_NODES.includes(node)) continue;
|
|
725
|
+
db.prepare("UPDATE dc_pipeline_nodes SET status = ?, updated_at = ? WHERE user_id = ? AND client_id = ? AND flow_node = ?").run(status, now(), uid, clientId, node);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function loadMessages(uid, clientId, node) {
|
|
729
|
+
const rows = db.prepare("SELECT payload FROM dc_node_messages WHERE user_id = ? AND client_id = ? AND flow_node = ? ORDER BY seq ASC").all(uid, clientId, node);
|
|
730
|
+
const out = [];
|
|
731
|
+
for (const r of rows) {
|
|
732
|
+
try {
|
|
733
|
+
out.push(JSON.parse(r.payload));
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return out;
|
|
738
|
+
}
|
|
739
|
+
function upsertMessages(uid, clientId, node, items) {
|
|
740
|
+
const ts = now();
|
|
741
|
+
const valid = items.filter((it) => it && it.id);
|
|
742
|
+
const ids = valid.map((it) => String(it.id));
|
|
743
|
+
const delSql = ids.length ? `DELETE FROM dc_node_messages WHERE user_id = ? AND client_id = ? AND flow_node = ? AND item_id NOT IN (${ids.map(() => "?").join(",")})` : "DELETE FROM dc_node_messages WHERE user_id = ? AND client_id = ? AND flow_node = ?";
|
|
744
|
+
const ins = db.prepare(`INSERT INTO dc_node_messages (user_id, client_id, flow_node, item_id, seq, payload, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
745
|
+
ON CONFLICT(user_id, client_id, flow_node, item_id) DO UPDATE SET seq = excluded.seq, payload = excluded.payload`);
|
|
746
|
+
const run = () => {
|
|
747
|
+
db.prepare(delSql).run(uid, clientId, node, ...ids);
|
|
748
|
+
valid.forEach((it, idx) => ins.run(uid, clientId, node, String(it.id), idx, JSON.stringify(it), ts));
|
|
749
|
+
};
|
|
750
|
+
db.exec("BEGIN");
|
|
751
|
+
try {
|
|
752
|
+
run();
|
|
753
|
+
db.exec("COMMIT");
|
|
754
|
+
} catch (e) {
|
|
755
|
+
db.exec("ROLLBACK");
|
|
756
|
+
throw e;
|
|
757
|
+
}
|
|
758
|
+
touchApp(uid, clientId);
|
|
759
|
+
}
|
|
760
|
+
function syncApp(uid, clientId, overview, opts) {
|
|
761
|
+
const info = overview?.appInfo;
|
|
762
|
+
const appName = info?.name || info?.appName || "";
|
|
763
|
+
const appState = info?.state || info?.status || "";
|
|
764
|
+
upsertUser(uid, {});
|
|
765
|
+
const existed = getApp(uid, clientId);
|
|
766
|
+
upsertApp(uid, clientId, { appName: appName || existed?.appName, appState });
|
|
767
|
+
ensureNodes(uid, clientId);
|
|
768
|
+
const prevNodes = getNodes(uid, clientId);
|
|
769
|
+
const hasLocal = prevNodes.some((n) => n.sessionId !== "" || (n.messageCount || 0) > 0);
|
|
770
|
+
if (!hasLocal) {
|
|
771
|
+
const { statuses, current } = deriveInitialNodes(appState, overview, opts?.cases);
|
|
772
|
+
setNodesStatus(uid, clientId, statuses);
|
|
773
|
+
const kd = overviewKeyData(overview);
|
|
774
|
+
for (const node of COMPARABLE_STEPS) updateNode(uid, clientId, node, { keyDataReplace: kd[node] });
|
|
775
|
+
setCurrentNode(uid, clientId, current);
|
|
776
|
+
return { firstOpen: true, currentNode: current, app: getApp(uid, clientId), nodes: getNodes(uid, clientId) };
|
|
777
|
+
}
|
|
778
|
+
return { firstOpen: false, app: getApp(uid, clientId), nodes: getNodes(uid, clientId) };
|
|
779
|
+
}
|
|
780
|
+
function migrateDraft(uid, clientId, appName, steps, executor) {
|
|
781
|
+
upsertUser(uid, {});
|
|
782
|
+
upsertApp(uid, clientId, { appName, ...executor ? { executor } : {} });
|
|
783
|
+
ensureNodes(uid, clientId);
|
|
784
|
+
for (const [node, data] of Object.entries(steps || {})) {
|
|
785
|
+
if (!FLOW_NODES.includes(node)) continue;
|
|
786
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
787
|
+
if (items.length > 0) upsertMessages(uid, clientId, node, items);
|
|
788
|
+
const created = node === "basic_info" && items.some((it) => it?.interaction?.type === "app_info" && it?.interaction?.clientId);
|
|
789
|
+
const existing = getNode(uid, clientId, node)?.status || "INIT";
|
|
790
|
+
let status = existing;
|
|
791
|
+
if (created) status = "DONE";
|
|
792
|
+
else if (existing === "INIT" && (items.length > 0 || data.sessionId)) {
|
|
793
|
+
const busy = db.prepare("SELECT COUNT(*) AS c FROM dc_pipeline_nodes WHERE user_id = ? AND client_id = ? AND status = 'IN_PROGRESS'").get(uid, clientId);
|
|
794
|
+
if ((busy?.c || 0) === 0) status = "IN_PROGRESS";
|
|
795
|
+
}
|
|
796
|
+
updateNode(uid, clientId, node, {
|
|
797
|
+
sessionId: data.sessionId || "",
|
|
798
|
+
status
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
upsertUser(uid, { activeClientId: clientId });
|
|
802
|
+
return getUser(uid);
|
|
803
|
+
}
|
|
804
|
+
var SETTING_CLI_PATH = "cli_path";
|
|
805
|
+
var SETTING_VERSION = "version";
|
|
806
|
+
var SETTING_EXECUTOR_PATHS = "executor_paths";
|
|
807
|
+
var SETTING_GIT_PATH = "git_path";
|
|
808
|
+
function getSetting(key) {
|
|
809
|
+
const r = db.prepare("SELECT value FROM dc_settings WHERE key = ?").get(key);
|
|
810
|
+
return r ? r.value ?? "" : void 0;
|
|
811
|
+
}
|
|
812
|
+
function setSetting(key, value) {
|
|
813
|
+
db.prepare(
|
|
814
|
+
`INSERT INTO dc_settings (key, value, updated_at) VALUES (?, ?, ?)
|
|
815
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
816
|
+
).run(key, value, now());
|
|
817
|
+
}
|
|
818
|
+
function getSettings() {
|
|
819
|
+
return {
|
|
820
|
+
cliPath: getSetting(SETTING_CLI_PATH) ?? "",
|
|
821
|
+
version: getSetting(SETTING_VERSION) ?? "v0.01"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function setSettings(patch) {
|
|
825
|
+
if (patch.cliPath !== void 0) setSetting(SETTING_CLI_PATH, patch.cliPath);
|
|
826
|
+
if (patch.version !== void 0) setSetting(SETTING_VERSION, patch.version);
|
|
827
|
+
return getSettings();
|
|
828
|
+
}
|
|
829
|
+
function getExecutorPaths() {
|
|
830
|
+
try {
|
|
831
|
+
const parsed = JSON.parse(getSetting(SETTING_EXECUTOR_PATHS) ?? "{}");
|
|
832
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
833
|
+
} catch {
|
|
834
|
+
return {};
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function setExecutorPath(key, path) {
|
|
838
|
+
const cur = getExecutorPaths();
|
|
839
|
+
if (path) cur[key] = path;
|
|
840
|
+
else delete cur[key];
|
|
841
|
+
setSetting(SETTING_EXECUTOR_PATHS, JSON.stringify(cur));
|
|
842
|
+
return cur;
|
|
843
|
+
}
|
|
844
|
+
function getGitPath() {
|
|
845
|
+
return getSetting(SETTING_GIT_PATH) ?? "";
|
|
846
|
+
}
|
|
847
|
+
function setGitPath(path) {
|
|
848
|
+
setSetting(SETTING_GIT_PATH, path || "");
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// server/cli/wfDeveloper.ts
|
|
852
|
+
var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
|
|
853
|
+
function findCommandPath(commands) {
|
|
854
|
+
for (const cmd of commands) {
|
|
855
|
+
try {
|
|
856
|
+
const path = execSync(`which ${cmd} 2>/dev/null`, { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
857
|
+
if (path && existsSync2(path)) return path;
|
|
858
|
+
} catch {
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
var WF_DEVELOPER_COMMANDS = ["wf-developer"];
|
|
864
|
+
var LEGACY_CLI_PATH_FILE = join3(__dirname3, "..", "..", "data", "wf-developer-path.txt");
|
|
865
|
+
function loadPersistedCliPath() {
|
|
866
|
+
const fromDb = getSettings().cliPath;
|
|
867
|
+
if (fromDb) return existsSync2(fromDb) ? fromDb : "";
|
|
868
|
+
try {
|
|
869
|
+
const legacy = readFileSync2(LEGACY_CLI_PATH_FILE, "utf-8").trim();
|
|
870
|
+
if (legacy && existsSync2(legacy)) {
|
|
871
|
+
setSettings({ cliPath: legacy });
|
|
872
|
+
return legacy;
|
|
873
|
+
}
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
return "";
|
|
877
|
+
}
|
|
878
|
+
var wfDeveloperUserPath = process.env.WF_DEVELOPER_BIN || loadPersistedCliPath();
|
|
879
|
+
function getWfDeveloperPath() {
|
|
880
|
+
return wfDeveloperUserPath;
|
|
881
|
+
}
|
|
882
|
+
function setWfDeveloperPath(p) {
|
|
883
|
+
const trimmed = (p || "").trim();
|
|
884
|
+
if (!trimmed) {
|
|
885
|
+
wfDeveloperUserPath = "";
|
|
886
|
+
try {
|
|
887
|
+
setSettings({ cliPath: "" });
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
return { ok: true, path: "" };
|
|
891
|
+
}
|
|
892
|
+
if (!existsSync2(trimmed)) return { ok: false, error: `\u8DEF\u5F84\u4E0D\u5B58\u5728: ${trimmed}` };
|
|
893
|
+
try {
|
|
894
|
+
if (!statSync(trimmed).isFile()) return { ok: false, error: `\u4E0D\u662F\u53EF\u6267\u884C\u6587\u4EF6: ${trimmed}` };
|
|
895
|
+
} catch (e) {
|
|
896
|
+
return { ok: false, error: `\u65E0\u6CD5\u8BBF\u95EE\u8BE5\u8DEF\u5F84: ${String(e)}` };
|
|
897
|
+
}
|
|
898
|
+
wfDeveloperUserPath = trimmed;
|
|
899
|
+
try {
|
|
900
|
+
setSettings({ cliPath: trimmed });
|
|
901
|
+
} catch (e) {
|
|
902
|
+
return { ok: false, error: `\u8DEF\u5F84\u6301\u4E45\u5316\u5931\u8D25: ${String(e)}` };
|
|
903
|
+
}
|
|
904
|
+
return { ok: true, path: trimmed };
|
|
905
|
+
}
|
|
906
|
+
var WF_BASE_URL_FILE = join3(__dirname3, "..", "..", "data", "wf-developer-base-url.txt");
|
|
907
|
+
function loadPersistedBaseUrl() {
|
|
908
|
+
try {
|
|
909
|
+
const v = readFileSync2(WF_BASE_URL_FILE, "utf-8").trim();
|
|
910
|
+
return /^https?:\/\//.test(v) ? v : "";
|
|
911
|
+
} catch {
|
|
912
|
+
return "";
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
var wfDeveloperBaseUrl = process.env.WF_DEVELOPER_BASE_URL || loadPersistedBaseUrl() || "https://developers.worldfirst.com";
|
|
916
|
+
function baseUrlArgs() {
|
|
917
|
+
return wfDeveloperBaseUrl ? ["--base-url", wfDeveloperBaseUrl] : [];
|
|
918
|
+
}
|
|
919
|
+
function getBaseUrl() {
|
|
920
|
+
return wfDeveloperBaseUrl;
|
|
921
|
+
}
|
|
922
|
+
function setBaseUrl(baseUrl) {
|
|
923
|
+
wfDeveloperBaseUrl = baseUrl.trim();
|
|
924
|
+
activeAppCache = null;
|
|
925
|
+
try {
|
|
926
|
+
mkdirSync2(dirname3(WF_BASE_URL_FILE), { recursive: true });
|
|
927
|
+
writeFileSync2(WF_BASE_URL_FILE, wfDeveloperBaseUrl, "utf-8");
|
|
928
|
+
} catch (e) {
|
|
929
|
+
console.warn("[wf-developer] base-url \u6301\u4E45\u5316\u5931\u8D25:", e);
|
|
930
|
+
}
|
|
931
|
+
return wfDeveloperBaseUrl;
|
|
932
|
+
}
|
|
933
|
+
function findWfDeveloper() {
|
|
934
|
+
if (wfDeveloperUserPath && existsSync2(wfDeveloperUserPath)) return wfDeveloperUserPath;
|
|
935
|
+
return findCommandPath(WF_DEVELOPER_COMMANDS);
|
|
936
|
+
}
|
|
937
|
+
var WF_IDENTITY_FILE = join3(__dirname3, "..", "..", "data", "wf-developer-identity.json");
|
|
938
|
+
function loadCachedIdentity(tokenExpiresAt) {
|
|
939
|
+
try {
|
|
940
|
+
const parsed = JSON.parse(readFileSync2(WF_IDENTITY_FILE, "utf-8"));
|
|
941
|
+
if (parsed && typeof parsed.serverUserId === "string" && parsed.serverUserId && parsed.tokenExpiresAt === tokenExpiresAt) {
|
|
942
|
+
return { serverUserId: parsed.serverUserId, email: typeof parsed.email === "string" ? parsed.email : "", tokenExpiresAt };
|
|
943
|
+
}
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
function saveCachedIdentity(identity) {
|
|
949
|
+
try {
|
|
950
|
+
mkdirSync2(dirname3(WF_IDENTITY_FILE), { recursive: true });
|
|
951
|
+
writeFileSync2(WF_IDENTITY_FILE, JSON.stringify(identity), "utf-8");
|
|
952
|
+
} catch (e) {
|
|
953
|
+
console.warn("[wf-developer] \u7528\u6237\u8EAB\u4EFD\u7F13\u5B58\u5199\u5165\u5931\u8D25:", e);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function clearCachedIdentity() {
|
|
957
|
+
try {
|
|
958
|
+
if (existsSync2(WF_IDENTITY_FILE)) unlinkSync(WF_IDENTITY_FILE);
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function parseWhoamiOutput(out) {
|
|
963
|
+
try {
|
|
964
|
+
const parsed = JSON.parse(out.trim());
|
|
965
|
+
if (!parsed.ok || !parsed.data) return null;
|
|
966
|
+
const expired = parsed.data.expired === true;
|
|
967
|
+
const authed = parsed.data.authenticated === true && !expired;
|
|
968
|
+
const type = parsed.data.type || (authed ? "OAuth" : "unknown");
|
|
969
|
+
const email = parsed.data.server_user_email || parsed.data.user_email || parsed.data.email || "";
|
|
970
|
+
const serverUserId = parsed.data.server_user_id || "";
|
|
971
|
+
const expiresAt = parsed.data.expires_at || "";
|
|
972
|
+
const serverVerified = parsed.data.server_verified === true;
|
|
973
|
+
return { authed, type, email, serverUserId, expired, expiresAt, serverVerified };
|
|
974
|
+
} catch {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function tryRefreshViaVerify(bin) {
|
|
979
|
+
try {
|
|
980
|
+
const v = spawnSync(bin, [...baseUrlArgs(), "whoami", "--verify", "--json"], { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] });
|
|
981
|
+
const server = parseWhoamiOutput(v.stdout || "");
|
|
982
|
+
if (!server?.serverVerified) return null;
|
|
983
|
+
const r2 = spawnSync(bin, [...baseUrlArgs(), "whoami", "--json"], { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] });
|
|
984
|
+
const fresh = parseWhoamiOutput(r2.stdout || "");
|
|
985
|
+
if (!fresh?.authed) return null;
|
|
986
|
+
fresh.email = server.email || fresh.email;
|
|
987
|
+
fresh.serverUserId = server.serverUserId || fresh.serverUserId;
|
|
988
|
+
return fresh;
|
|
989
|
+
} catch {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
var WHOAMI_CACHE_TTL_MS = 60 * 1e3;
|
|
994
|
+
var whoamiCache = null;
|
|
995
|
+
var refreshFlight = null;
|
|
996
|
+
function whoamiNow(bin) {
|
|
997
|
+
const r = spawnSync(bin, [...baseUrlArgs(), "whoami", "--json"], { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] });
|
|
998
|
+
return parseWhoamiOutput(r.stdout || "");
|
|
999
|
+
}
|
|
1000
|
+
function ensureFreshCredentials(bin) {
|
|
1001
|
+
if (whoamiCache && !whoamiCache.expired && Date.now() - whoamiCache.at < WHOAMI_CACHE_TTL_MS) return Promise.resolve();
|
|
1002
|
+
if (!refreshFlight) {
|
|
1003
|
+
refreshFlight = Promise.resolve().then(() => {
|
|
1004
|
+
const local = whoamiNow(bin);
|
|
1005
|
+
if (local?.expired) {
|
|
1006
|
+
if (tryRefreshViaVerify(bin)) console.log("[wf-developer] \u4E1A\u52A1\u8C03\u7528\u9884\u68C0\uFF1Aaccess token \u5DF2\u8FC7\u671F\uFF0C\u5DF2\u7528 refresh token \u9759\u9ED8\u5237\u65B0");
|
|
1007
|
+
}
|
|
1008
|
+
const fresh = whoamiNow(bin);
|
|
1009
|
+
whoamiCache = { at: Date.now(), expired: !!fresh?.expired };
|
|
1010
|
+
}).finally(() => {
|
|
1011
|
+
refreshFlight = null;
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
return refreshFlight;
|
|
1015
|
+
}
|
|
1016
|
+
function invalidateWhoamiCacheIfCredentialError(error) {
|
|
1017
|
+
if (error && /no saved credentials|expired|refresh/i.test(error)) whoamiCache = null;
|
|
1018
|
+
}
|
|
1019
|
+
function detectWfDeveloperSync() {
|
|
1020
|
+
const bin = findWfDeveloper();
|
|
1021
|
+
if (!bin) {
|
|
1022
|
+
return { installed: false, loggedIn: false, baseUrl: wfDeveloperBaseUrl, detail: "\u672A\u5728 PATH \u4E2D\u68C0\u6D4B\u5230 wf-developer CLI\uFF0C\u8BF7\u5728\u542F\u52A8\u68C0\u67E5\u9875\u624B\u52A8\u6307\u5B9A\u5176\u5B89\u88C5\u8DEF\u5F84" };
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
const r = spawnSync(bin, [...baseUrlArgs(), "whoami", "--json"], { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] });
|
|
1026
|
+
let local = parseWhoamiOutput(r.stdout || "");
|
|
1027
|
+
if (!local || !local.authed) console.log("[wf-developer] whoami \u539F\u59CB\u8F93\u51FA:", JSON.stringify({ status: r.status, error: String(r.error || ""), stdout: (r.stdout || "").slice(0, 400), stderr: (r.stderr || "").slice(0, 200) }));
|
|
1028
|
+
if (!local) return { installed: true, loggedIn: false, baseUrl: wfDeveloperBaseUrl, detail: "\u6388\u6743\u72B6\u6001\u672A\u77E5" };
|
|
1029
|
+
if (local.expired) {
|
|
1030
|
+
const refreshed = tryRefreshViaVerify(bin);
|
|
1031
|
+
if (refreshed) {
|
|
1032
|
+
local = refreshed;
|
|
1033
|
+
console.log("[wf-developer] access token \u5DF2\u8FC7\u671F\uFF0C\u5DF2\u7528 refresh token \u9759\u9ED8\u5237\u65B0");
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (!local.authed) {
|
|
1037
|
+
clearCachedIdentity();
|
|
1038
|
+
return {
|
|
1039
|
+
installed: true,
|
|
1040
|
+
loggedIn: false,
|
|
1041
|
+
authType: local.type,
|
|
1042
|
+
baseUrl: wfDeveloperBaseUrl,
|
|
1043
|
+
detail: local.expired ? "\u51ED\u8BC1\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u70B9\u51FB\u6388\u6743\u6309\u94AE\u91CD\u65B0\u6388\u6743" : "\u672A\u6388\u6743\uFF0C\u8BF7\u70B9\u51FB\u6388\u6743\u6309\u94AE\u5B8C\u6210\u6388\u6743"
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
let identity = loadCachedIdentity(local.expiresAt);
|
|
1047
|
+
if (!identity && local.serverUserId) {
|
|
1048
|
+
identity = { serverUserId: local.serverUserId, email: local.email, tokenExpiresAt: local.expiresAt };
|
|
1049
|
+
}
|
|
1050
|
+
if (!identity) {
|
|
1051
|
+
try {
|
|
1052
|
+
const v = spawnSync(bin, [...baseUrlArgs(), "whoami", "--verify", "--json"], { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] });
|
|
1053
|
+
const server = parseWhoamiOutput(v.stdout || "");
|
|
1054
|
+
if (server?.authed && (server.serverUserId || server.email)) {
|
|
1055
|
+
identity = { serverUserId: server.serverUserId, email: server.email, tokenExpiresAt: local.expiresAt };
|
|
1056
|
+
}
|
|
1057
|
+
} catch {
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (identity?.serverUserId) saveCachedIdentity(identity);
|
|
1061
|
+
if (identity) {
|
|
1062
|
+
if (!local.email) local.email = identity.email;
|
|
1063
|
+
local.serverUserId = identity.serverUserId;
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
installed: true,
|
|
1067
|
+
loggedIn: true,
|
|
1068
|
+
authType: local.type,
|
|
1069
|
+
email: local.email || void 0,
|
|
1070
|
+
serverUserId: local.serverUserId || void 0,
|
|
1071
|
+
baseUrl: wfDeveloperBaseUrl,
|
|
1072
|
+
detail: `\u5DF2\u6388\u6743 (${local.email || local.type})`
|
|
1073
|
+
};
|
|
1074
|
+
} catch {
|
|
1075
|
+
return { installed: true, loggedIn: false, baseUrl: wfDeveloperBaseUrl, detail: "\u68C0\u6D4B\u6388\u6743\u72B6\u6001\u5931\u8D25" };
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function runWfDeveloper(args, timeoutMs = 6e4) {
|
|
1079
|
+
const bin = findWfDeveloper();
|
|
1080
|
+
if (!bin) {
|
|
1081
|
+
return Promise.resolve({ ok: false, error: `\u672A\u627E\u5230 wf-developer CLI\uFF08PATH: ${WF_DEVELOPER_COMMANDS.join("/")}\uFF1B\u53EF\u5728\u542F\u52A8\u68C0\u67E5\u9875\u624B\u52A8\u6307\u5B9A\u5B89\u88C5\u8DEF\u5F84\uFF09` });
|
|
1082
|
+
}
|
|
1083
|
+
return ensureFreshCredentials(bin).then(() => new Promise((resolve3) => {
|
|
1084
|
+
const fullArgs = [...baseUrlArgs(), ...args, "--json"];
|
|
1085
|
+
const cmdStr = `wf-developer ${fullArgs.join(" ")}`;
|
|
1086
|
+
console.log(`[CLI] ${cmdStr}`);
|
|
1087
|
+
const child = spawn(bin, fullArgs, { env: { ...process.env, WF_HTTP_DEBUG: "1" }, stdio: ["ignore", "pipe", "pipe"] });
|
|
1088
|
+
let stdout = "", stderr = "", settled = false;
|
|
1089
|
+
const timer = setTimeout(() => {
|
|
1090
|
+
try {
|
|
1091
|
+
child.kill("SIGKILL");
|
|
1092
|
+
} catch {
|
|
1093
|
+
}
|
|
1094
|
+
}, timeoutMs);
|
|
1095
|
+
timer.unref?.();
|
|
1096
|
+
const finish = () => {
|
|
1097
|
+
if (settled) return;
|
|
1098
|
+
settled = true;
|
|
1099
|
+
clearTimeout(timer);
|
|
1100
|
+
const raw = stdout.trim();
|
|
1101
|
+
const httpDebug = [];
|
|
1102
|
+
const keptErr = [];
|
|
1103
|
+
for (const line of stderr.split("\n")) {
|
|
1104
|
+
const t = line.trim();
|
|
1105
|
+
if (t.startsWith("{") && t.includes('"wf_http_debug"')) {
|
|
1106
|
+
try {
|
|
1107
|
+
const o = JSON.parse(t);
|
|
1108
|
+
if (o && o.type === "wf_http_debug") {
|
|
1109
|
+
httpDebug.push(o);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
keptErr.push(line);
|
|
1116
|
+
}
|
|
1117
|
+
const errRaw = keptErr.join("\n").trim();
|
|
1118
|
+
const candidates = [];
|
|
1119
|
+
for (const text of [raw, errRaw]) {
|
|
1120
|
+
if (!text) continue;
|
|
1121
|
+
candidates.push(text);
|
|
1122
|
+
const firstBrace = text.indexOf("{");
|
|
1123
|
+
if (firstBrace > 0) candidates.push(text.slice(firstBrace));
|
|
1124
|
+
}
|
|
1125
|
+
for (const candidate of candidates) {
|
|
1126
|
+
try {
|
|
1127
|
+
const parsed = JSON.parse(candidate);
|
|
1128
|
+
const traceId = parsed.sofaTraceId || parsed.sofa_trace_id || void 0;
|
|
1129
|
+
if (parsed.ok) {
|
|
1130
|
+
resolve3({ ok: true, data: parsed.data, cmd: cmdStr, traceId, httpDebug });
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const err = parsed.error?.message || parsed.error?.code || errRaw || "wf-developer \u6267\u884C\u5931\u8D25";
|
|
1134
|
+
invalidateWhoamiCacheIfCredentialError(err);
|
|
1135
|
+
resolve3({ ok: false, error: err, cmd: cmdStr, traceId, httpDebug });
|
|
1136
|
+
return;
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
resolve3({ ok: false, error: `wf-developer \u8F93\u51FA\u89E3\u6790\u5931\u8D25: ${(errRaw || raw).slice(0, 500)}`, cmd: cmdStr });
|
|
1141
|
+
};
|
|
1142
|
+
child.stdout.on("data", (c) => {
|
|
1143
|
+
stdout = `${stdout}${c.toString()}`.slice(-512 * 1024);
|
|
1144
|
+
});
|
|
1145
|
+
child.stderr.on("data", (c) => {
|
|
1146
|
+
stderr = `${stderr}${c.toString()}`.slice(-64 * 1024);
|
|
1147
|
+
});
|
|
1148
|
+
child.on("error", (e) => {
|
|
1149
|
+
if (!settled) resolve3({ ok: false, error: String(e), cmd: cmdStr });
|
|
1150
|
+
settled = true;
|
|
1151
|
+
clearTimeout(timer);
|
|
1152
|
+
});
|
|
1153
|
+
child.on("close", () => finish());
|
|
1154
|
+
}));
|
|
1155
|
+
}
|
|
1156
|
+
function sendWfResult(res, r) {
|
|
1157
|
+
if (r.cmd) {
|
|
1158
|
+
const traceSuffix = r.traceId ? ` | sofaTraceId: ${r.traceId}` : "";
|
|
1159
|
+
res.setHeader("X-Wf-Cli", encodeURIComponent(r.cmd + traceSuffix));
|
|
1160
|
+
}
|
|
1161
|
+
const debug = r.httpDebug?.length ? { httpDebug: r.httpDebug } : {};
|
|
1162
|
+
if (r.ok) res.json({ success: true, data: r.data ?? {}, ...debug });
|
|
1163
|
+
else res.status(502).json({ success: false, error: r.error || "wf-developer \u8C03\u7528\u5931\u8D25", ...debug });
|
|
1164
|
+
}
|
|
1165
|
+
var activeAppCache = null;
|
|
1166
|
+
function ensureActiveApp(bin, appId) {
|
|
1167
|
+
if (activeAppCache && activeAppCache.appId === appId && activeAppCache.baseUrl === wfDeveloperBaseUrl) return { ok: true };
|
|
1168
|
+
const useArgs = [...baseUrlArgs(), "app", "use", appId, "--json"];
|
|
1169
|
+
let r = spawnSync(bin, useArgs, { encoding: "utf-8", timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] });
|
|
1170
|
+
if (r.status !== 0 && /not synchronized/i.test(`${r.stdout || ""}${r.stderr || ""}`)) {
|
|
1171
|
+
const sync = spawnSync(bin, [...baseUrlArgs(), "login", "--json"], { encoding: "utf-8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
1172
|
+
if (sync.status === 0) {
|
|
1173
|
+
r = spawnSync(bin, useArgs, { encoding: "utf-8", timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] });
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (r.status !== 0) {
|
|
1177
|
+
return { ok: false, error: (r.stdout || r.stderr || "").trim().slice(0, 300) };
|
|
1178
|
+
}
|
|
1179
|
+
activeAppCache = { appId, baseUrl: wfDeveloperBaseUrl };
|
|
1180
|
+
return { ok: true };
|
|
1181
|
+
}
|
|
1182
|
+
async function runWfDeveloperForApp(args, appId, timeoutMs = 6e4) {
|
|
1183
|
+
const bin = findWfDeveloper();
|
|
1184
|
+
if (!bin) {
|
|
1185
|
+
return { ok: false, error: `\u672A\u627E\u5230 wf-developer CLI\uFF08PATH: ${WF_DEVELOPER_COMMANDS.join("/")}\uFF1B\u53EF\u5728\u542F\u52A8\u68C0\u67E5\u9875\u624B\u52A8\u6307\u5B9A\u5B89\u88C5\u8DEF\u5F84\uFF09` };
|
|
1186
|
+
}
|
|
1187
|
+
await ensureFreshCredentials(bin);
|
|
1188
|
+
const sel = ensureActiveApp(bin, appId);
|
|
1189
|
+
if (!sel.ok) {
|
|
1190
|
+
return { ok: false, error: `\u9009\u62E9\u5E94\u7528\u5931\u8D25 (wf-developer app use ${appId}): ${sel.error}` };
|
|
1191
|
+
}
|
|
1192
|
+
let result = await runWfDeveloper(args, timeoutMs);
|
|
1193
|
+
if (!result.ok && /not synchronized/i.test(result.error || "")) {
|
|
1194
|
+
const sync = spawnSync(bin, [...baseUrlArgs(), "login", "--json"], {
|
|
1195
|
+
encoding: "utf-8",
|
|
1196
|
+
timeout: 6e4,
|
|
1197
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1198
|
+
});
|
|
1199
|
+
if (sync.status === 0) {
|
|
1200
|
+
whoamiCache = null;
|
|
1201
|
+
activeAppCache = null;
|
|
1202
|
+
const reselect = ensureActiveApp(bin, appId);
|
|
1203
|
+
result = reselect.ok ? await runWfDeveloper(args, timeoutMs) : { ok: false, error: `\u9009\u62E9\u5E94\u7528\u5931\u8D25 (wf-developer app use ${appId}): ${reselect.error}` };
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// server/routes/testcase.ts
|
|
1210
|
+
import { writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1211
|
+
import { join as join4 } from "node:path";
|
|
1212
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1213
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1214
|
+
|
|
1215
|
+
// server/cli/routeFactory.ts
|
|
1216
|
+
function defineCliRoute(app2, spec) {
|
|
1217
|
+
const source = spec.method === "get" ? "query" : "body";
|
|
1218
|
+
app2[spec.method](spec.path, async (req, res) => {
|
|
1219
|
+
const raw = source === "query" ? req.query : req.body;
|
|
1220
|
+
const values = {};
|
|
1221
|
+
const missing = [];
|
|
1222
|
+
for (const [name, opt] of Object.entries(spec.params || {})) {
|
|
1223
|
+
const v = raw?.[name];
|
|
1224
|
+
const str = v == null ? "" : String(v).trim();
|
|
1225
|
+
if (!str) {
|
|
1226
|
+
if (opt.required) missing.push(name);
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
if (opt.numeric && !Number.isFinite(Number(str))) {
|
|
1230
|
+
res.status(400).json({ success: false, error: `${name} \u5FC5\u987B\u662F\u6570\u5B57` });
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
values[name] = str;
|
|
1234
|
+
}
|
|
1235
|
+
if (missing.length > 0) {
|
|
1236
|
+
res.status(400).json({ success: false, error: `${missing.join("\u3001")} \u4E3A\u5FC5\u586B` });
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const args = spec.args(values);
|
|
1240
|
+
const result = spec.run ? await spec.run(args, values, spec.timeoutMs) : await runWfDeveloper(args, spec.timeoutMs);
|
|
1241
|
+
sendWfResult(res, result);
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// server/routes/testcase.ts
|
|
1246
|
+
async function withTempJson(prefix, payload, run) {
|
|
1247
|
+
const file = join4(tmpdir2(), `${prefix}-${randomUUID2()}.json`);
|
|
1248
|
+
try {
|
|
1249
|
+
writeFileSync3(file, JSON.stringify(payload), "utf-8");
|
|
1250
|
+
await run(file);
|
|
1251
|
+
} finally {
|
|
1252
|
+
try {
|
|
1253
|
+
unlinkSync2(file);
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function registerTestcaseRoutes(app2) {
|
|
1259
|
+
app2.post("/api/testcase/init", async (req, res) => {
|
|
1260
|
+
const { clientId, cases } = req.body;
|
|
1261
|
+
if (!clientId || !Array.isArray(cases) || cases.length === 0) {
|
|
1262
|
+
res.status(400).json({ success: false, error: "clientId \u548C cases\uFF08\u975E\u7A7A\u6570\u7EC4\uFF09\u4E3A\u5FC5\u586B" });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
await withTempJson("wf-testcase-init", { clientId, cases }, async (file) => {
|
|
1267
|
+
sendWfResult(res, await runWfDeveloperForApp(["app", "testcase", "init", "--file", file, "--client-id", clientId], clientId));
|
|
1268
|
+
});
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
app2.post("/api/testcase/add", async (req, res) => {
|
|
1274
|
+
const { clientId, case: c } = req.body;
|
|
1275
|
+
if (!clientId || !c?.caseKey || !c?.caseName) {
|
|
1276
|
+
res.status(400).json({ success: false, error: "clientId \u548C case\uFF08\u542B caseKey/caseName\uFF09\u4E3A\u5FC5\u586B" });
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
await withTempJson("wf-testcase-steps", c.steps || [], async (file) => {
|
|
1281
|
+
const args = [
|
|
1282
|
+
"app",
|
|
1283
|
+
"testcase",
|
|
1284
|
+
"add",
|
|
1285
|
+
"--case-key",
|
|
1286
|
+
c.caseKey,
|
|
1287
|
+
"--name",
|
|
1288
|
+
c.caseName,
|
|
1289
|
+
"--type",
|
|
1290
|
+
c.caseType || "happy",
|
|
1291
|
+
"--desc",
|
|
1292
|
+
c.description || "",
|
|
1293
|
+
"--steps",
|
|
1294
|
+
file,
|
|
1295
|
+
"--client-id",
|
|
1296
|
+
clientId
|
|
1297
|
+
];
|
|
1298
|
+
if (c.required) args.push("--required");
|
|
1299
|
+
sendWfResult(res, await runWfDeveloperForApp(args, clientId));
|
|
1300
|
+
});
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
defineCliRoute(app2, {
|
|
1306
|
+
method: "post",
|
|
1307
|
+
path: "/api/testcase/skip",
|
|
1308
|
+
params: { clientId: { required: true }, caseKey: { required: true } },
|
|
1309
|
+
args: (p) => ["app", "testcase", "skip", "--case-key", p.caseKey, "--client-id", p.clientId],
|
|
1310
|
+
run: (args, p, t) => runWfDeveloperForApp(args, p.clientId, t)
|
|
1311
|
+
});
|
|
1312
|
+
defineCliRoute(app2, {
|
|
1313
|
+
method: "post",
|
|
1314
|
+
path: "/api/testcase/start",
|
|
1315
|
+
params: { clientId: { required: true }, caseKey: { required: true } },
|
|
1316
|
+
args: (p) => ["app", "testcase", "start", "--case-key", p.caseKey, "--client-id", p.clientId],
|
|
1317
|
+
run: (args, p, t) => runWfDeveloperForApp(args, p.clientId, t)
|
|
1318
|
+
});
|
|
1319
|
+
defineCliRoute(app2, {
|
|
1320
|
+
method: "get",
|
|
1321
|
+
path: "/api/testcase/list",
|
|
1322
|
+
params: { clientId: { required: true }, status: {} },
|
|
1323
|
+
args: (p) => ["app", "testcase", "list", "--client-id", p.clientId, ...p.status ? ["--status", p.status] : []],
|
|
1324
|
+
run: (args, p, t) => runWfDeveloperForApp(args, p.clientId, t)
|
|
1325
|
+
});
|
|
1326
|
+
app2.post("/api/testcase/progress/update", async (req, res) => {
|
|
1327
|
+
const { clientId, caseKey, stepSeq, status, logId, logTime, failReason, request, response, traceId, logType } = req.body || {};
|
|
1328
|
+
if (!clientId || !caseKey || stepSeq == null || !status) {
|
|
1329
|
+
res.status(400).json({ success: false, error: "clientId\u3001caseKey\u3001stepSeq\u3001status \u4E3A\u5FC5\u586B" });
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const baseArgs = [
|
|
1333
|
+
"app",
|
|
1334
|
+
"testcase",
|
|
1335
|
+
"update_progress",
|
|
1336
|
+
"--case-key",
|
|
1337
|
+
caseKey,
|
|
1338
|
+
"--client-id",
|
|
1339
|
+
clientId,
|
|
1340
|
+
"--step-seq",
|
|
1341
|
+
String(stepSeq),
|
|
1342
|
+
"--status",
|
|
1343
|
+
status,
|
|
1344
|
+
...logId ? ["--log-id", logId] : [],
|
|
1345
|
+
...logTime ? ["--log-time", logTime] : [],
|
|
1346
|
+
...failReason ? ["--fail-reason", failReason] : []
|
|
1347
|
+
];
|
|
1348
|
+
const hasPayload = typeof request === "string" && request.length > 0;
|
|
1349
|
+
try {
|
|
1350
|
+
if (hasPayload) {
|
|
1351
|
+
await withTempJson("wf-testcase-payload", { request, response: response || "", traceId: traceId || "", logType: logType || "", logTime: logTime || "" }, async (file) => {
|
|
1352
|
+
sendWfResult(res, await runWfDeveloperForApp([...baseArgs, "--payload-file", file], clientId));
|
|
1353
|
+
});
|
|
1354
|
+
} else {
|
|
1355
|
+
sendWfResult(res, await runWfDeveloperForApp(baseArgs, clientId));
|
|
1356
|
+
}
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// server/routes/files.ts
|
|
1364
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1365
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4, readdirSync, statSync as statSync2, realpathSync } from "node:fs";
|
|
1366
|
+
import { join as join5, dirname as dirname4, isAbsolute, resolve } from "node:path";
|
|
1367
|
+
function registerFileRoutes(app2) {
|
|
1368
|
+
const FILE_TREE_EXCLUDES = /* @__PURE__ */ new Set([
|
|
1369
|
+
"node_modules",
|
|
1370
|
+
".git",
|
|
1371
|
+
"target",
|
|
1372
|
+
"dist",
|
|
1373
|
+
"build",
|
|
1374
|
+
"out",
|
|
1375
|
+
"bin",
|
|
1376
|
+
".idea",
|
|
1377
|
+
".vscode",
|
|
1378
|
+
".qoder",
|
|
1379
|
+
".gradle",
|
|
1380
|
+
".settings",
|
|
1381
|
+
".classpath",
|
|
1382
|
+
".project",
|
|
1383
|
+
"__pycache__",
|
|
1384
|
+
".DS_Store",
|
|
1385
|
+
"coverage",
|
|
1386
|
+
".next",
|
|
1387
|
+
".nuxt",
|
|
1388
|
+
"logs"
|
|
1389
|
+
]);
|
|
1390
|
+
function resolveWithinRoot(root, target) {
|
|
1391
|
+
const resolvedRoot = resolve(root);
|
|
1392
|
+
const resolvedTarget = isAbsolute(target) ? resolve(target) : resolve(resolvedRoot, target);
|
|
1393
|
+
const within = (r, t) => t === r || t.startsWith(r + "/");
|
|
1394
|
+
if (within(resolvedRoot, resolvedTarget)) return resolvedTarget;
|
|
1395
|
+
try {
|
|
1396
|
+
const realRoot = realpathSync(resolvedRoot);
|
|
1397
|
+
let existing = resolvedTarget;
|
|
1398
|
+
while (!existsSync3(existing)) {
|
|
1399
|
+
const parent = dirname4(existing);
|
|
1400
|
+
if (parent === existing) return null;
|
|
1401
|
+
existing = parent;
|
|
1402
|
+
}
|
|
1403
|
+
const realTarget = realpathSync(existing) + resolvedTarget.slice(existing.length);
|
|
1404
|
+
if (within(realRoot, realTarget)) return resolvedTarget;
|
|
1405
|
+
return null;
|
|
1406
|
+
} catch {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function validateRoot(raw) {
|
|
1411
|
+
if (typeof raw !== "string" || !raw.trim() || !isAbsolute(raw.trim())) return null;
|
|
1412
|
+
const root = resolve(raw.trim());
|
|
1413
|
+
try {
|
|
1414
|
+
if (!existsSync3(root) || !statSync2(root).isDirectory()) return null;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
return root;
|
|
1419
|
+
}
|
|
1420
|
+
app2.get("/api/files/tree", (req, res) => {
|
|
1421
|
+
const root = validateRoot(req.query.root);
|
|
1422
|
+
if (!root) {
|
|
1423
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
const dir = typeof req.query.dir === "string" && req.query.dir ? req.query.dir : root;
|
|
1427
|
+
const safeDir = resolveWithinRoot(root, dir);
|
|
1428
|
+
if (!safeDir) {
|
|
1429
|
+
res.status(403).json({ success: false, error: "\u8DEF\u5F84\u8D8A\u754C\uFF1A\u4EC5\u5141\u8BB8\u8BBF\u95EE\u9879\u76EE\u76EE\u5F55\u5185\u7684\u6587\u4EF6" });
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
try {
|
|
1433
|
+
const entries = readdirSync(safeDir, { withFileTypes: true }).filter((ent) => !FILE_TREE_EXCLUDES.has(ent.name) && !ent.name.startsWith(".git")).slice(0, 500).map((ent) => {
|
|
1434
|
+
const full = join5(safeDir, ent.name);
|
|
1435
|
+
if (ent.isDirectory()) return { name: ent.name, path: full, type: "dir" };
|
|
1436
|
+
let size;
|
|
1437
|
+
let mtime;
|
|
1438
|
+
try {
|
|
1439
|
+
const st = statSync2(full);
|
|
1440
|
+
size = st.size;
|
|
1441
|
+
mtime = st.mtimeMs;
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
return { name: ent.name, path: full, type: "file", size, mtime };
|
|
1445
|
+
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
|
1446
|
+
res.json({ success: true, data: { root, dir: safeDir, items: entries } });
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
const FILE_READ_LIMIT = 2 * 1024 * 1024;
|
|
1452
|
+
app2.get("/api/files/content", (req, res) => {
|
|
1453
|
+
const root = validateRoot(req.query.root);
|
|
1454
|
+
if (!root) {
|
|
1455
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const filePath = typeof req.query.path === "string" ? req.query.path : "";
|
|
1459
|
+
const safePath = resolveWithinRoot(root, filePath);
|
|
1460
|
+
if (!safePath) {
|
|
1461
|
+
res.status(403).json({ success: false, error: "\u8DEF\u5F84\u8D8A\u754C\uFF1A\u4EC5\u5141\u8BB8\u8BBF\u95EE\u9879\u76EE\u76EE\u5F55\u5185\u7684\u6587\u4EF6" });
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
try {
|
|
1465
|
+
const st = statSync2(safePath);
|
|
1466
|
+
if (!st.isFile()) {
|
|
1467
|
+
res.status(400).json({ success: false, error: "\u76EE\u6807\u4E0D\u662F\u6587\u4EF6" });
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (st.size > FILE_READ_LIMIT) {
|
|
1471
|
+
res.status(413).json({ success: false, error: `\u6587\u4EF6\u8FC7\u5927\uFF08${(st.size / 1024 / 1024).toFixed(1)}MB\uFF09\uFF0C\u8D85\u8FC7 2MB \u9650\u5236` });
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
const buf = readFileSync3(safePath);
|
|
1475
|
+
if (buf.includes(0)) {
|
|
1476
|
+
res.status(415).json({ success: false, error: "\u4E8C\u8FDB\u5236\u6587\u4EF6\u4E0D\u652F\u6301\u5728\u7EBF\u67E5\u770B/\u7F16\u8F91" });
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
res.json({ success: true, data: { path: safePath, content: buf.toString("utf-8"), size: st.size, mtime: st.mtimeMs, exists: true } });
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
if (err?.code === "ENOENT") {
|
|
1482
|
+
res.json({ success: true, data: { path: safePath, content: "", size: 0, exists: false } });
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
res.status(404).json({ success: false, error: `\u8BFB\u53D6\u5931\u8D25: ${String(err)}` });
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
app2.put("/api/files/content", (req, res) => {
|
|
1489
|
+
const { root: rawRoot, path: rawPath, content } = req.body;
|
|
1490
|
+
const root = validateRoot(rawRoot);
|
|
1491
|
+
if (!root) {
|
|
1492
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const safePath = resolveWithinRoot(root, rawPath || "");
|
|
1496
|
+
if (!safePath) {
|
|
1497
|
+
res.status(403).json({ success: false, error: "\u8DEF\u5F84\u8D8A\u754C\uFF1A\u4EC5\u5141\u8BB8\u4FDD\u5B58\u9879\u76EE\u76EE\u5F55\u5185\u7684\u6587\u4EF6" });
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (typeof content !== "string") {
|
|
1501
|
+
res.status(400).json({ success: false, error: "content \u5FC5\u987B\u4E3A\u5B57\u7B26\u4E32" });
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (Buffer.byteLength(content, "utf-8") > FILE_READ_LIMIT) {
|
|
1505
|
+
res.status(413).json({ success: false, error: "\u5185\u5BB9\u8D85\u8FC7 2MB \u9650\u5236" });
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
try {
|
|
1509
|
+
if (!existsSync3(dirname4(safePath))) {
|
|
1510
|
+
res.status(400).json({ success: false, error: "\u7236\u76EE\u5F55\u4E0D\u5B58\u5728" });
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
writeFileSync4(safePath, content, "utf-8");
|
|
1514
|
+
const st = statSync2(safePath);
|
|
1515
|
+
res.json({ success: true, data: { path: safePath, size: st.size, mtime: st.mtimeMs } });
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
res.status(500).json({ success: false, error: `\u4FDD\u5B58\u5931\u8D25: ${String(err)}` });
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
function gitRead(root, args) {
|
|
1521
|
+
try {
|
|
1522
|
+
const r = spawnSync2("git", ["-C", root, ...args], { encoding: "utf-8", timeout: 1e4, maxBuffer: 16 * 1024 * 1024 });
|
|
1523
|
+
if (r.status !== 0) return null;
|
|
1524
|
+
return r.stdout || "";
|
|
1525
|
+
} catch {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function relToGitRepo(root, absPath) {
|
|
1530
|
+
const top = gitRead(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
1531
|
+
if (!top) return null;
|
|
1532
|
+
let realTop = top;
|
|
1533
|
+
try {
|
|
1534
|
+
realTop = realpathSync(top);
|
|
1535
|
+
} catch {
|
|
1536
|
+
}
|
|
1537
|
+
let realAbs = absPath;
|
|
1538
|
+
try {
|
|
1539
|
+
realAbs = realpathSync(absPath);
|
|
1540
|
+
} catch {
|
|
1541
|
+
try {
|
|
1542
|
+
const realRoot = realpathSync(root);
|
|
1543
|
+
if (absPath.startsWith(root + "/")) realAbs = realRoot + absPath.slice(root.length);
|
|
1544
|
+
} catch {
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (realAbs.startsWith(realTop + "/")) return realAbs.slice(realTop.length + 1);
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
function countLines(content) {
|
|
1551
|
+
if (!content) return 0;
|
|
1552
|
+
const n = content.split("\n").length;
|
|
1553
|
+
return content.endsWith("\n") ? n - 1 : n;
|
|
1554
|
+
}
|
|
1555
|
+
function parsePorcelainStatus(xy) {
|
|
1556
|
+
if (xy === "??" || xy.includes("A")) return "added";
|
|
1557
|
+
if (xy.includes("D")) return "deleted";
|
|
1558
|
+
return "modified";
|
|
1559
|
+
}
|
|
1560
|
+
app2.get("/api/files/changes", (req, res) => {
|
|
1561
|
+
const root = validateRoot(req.query.root);
|
|
1562
|
+
if (!root) {
|
|
1563
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const rawPaths = typeof req.query.paths === "string" ? req.query.paths : "";
|
|
1567
|
+
const paths = [...new Set(
|
|
1568
|
+
rawPaths.split(",").map((p) => p.trim()).filter(Boolean).map((p) => resolveWithinRoot(root, p)).filter((p) => !!p)
|
|
1569
|
+
)];
|
|
1570
|
+
const isGit = gitRead(root, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
|
|
1571
|
+
if (!isGit || paths.length === 0) {
|
|
1572
|
+
res.json({ success: true, data: { git: false, files: [] } });
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const files = paths.map((abs) => {
|
|
1576
|
+
const status = parsePorcelainStatus((gitRead(root, ["status", "--porcelain", "--", abs]) || "").slice(0, 2));
|
|
1577
|
+
let additions = 0, deletions = 0;
|
|
1578
|
+
const numstat = gitRead(root, ["diff", "--numstat", "HEAD", "--", abs]);
|
|
1579
|
+
if (numstat && numstat.trim()) {
|
|
1580
|
+
const [a, d] = numstat.trim().split("\n")[0].split(" ");
|
|
1581
|
+
additions = a === "-" ? 0 : parseInt(a, 10) || 0;
|
|
1582
|
+
deletions = d === "-" ? 0 : parseInt(d, 10) || 0;
|
|
1583
|
+
} else if (status === "added") {
|
|
1584
|
+
try {
|
|
1585
|
+
const buf = readFileSync3(abs);
|
|
1586
|
+
if (!buf.includes(0)) additions = countLines(buf.toString("utf-8"));
|
|
1587
|
+
} catch {
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return { path: abs, status, additions, deletions };
|
|
1591
|
+
});
|
|
1592
|
+
res.json({ success: true, data: { git: true, files } });
|
|
1593
|
+
});
|
|
1594
|
+
app2.post("/api/files/content", (req, res) => {
|
|
1595
|
+
const { root: rawRoot, path: rawPath, content } = req.body;
|
|
1596
|
+
const root = validateRoot(rawRoot);
|
|
1597
|
+
if (!root) {
|
|
1598
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (typeof content !== "string") {
|
|
1602
|
+
res.status(400).json({ success: false, error: "\u7F3A\u5C11 content" });
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const safePath = resolveWithinRoot(root, typeof rawPath === "string" ? rawPath : "");
|
|
1606
|
+
if (!safePath) {
|
|
1607
|
+
res.status(403).json({ success: false, error: "\u8DEF\u5F84\u8D8A\u754C\uFF1A\u4EC5\u5141\u8BB8\u8BBF\u95EE\u9879\u76EE\u76EE\u5F55\u5185\u7684\u6587\u4EF6" });
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
try {
|
|
1611
|
+
writeFileSync4(safePath, content, "utf-8");
|
|
1612
|
+
res.json({ success: true, data: { path: safePath } });
|
|
1613
|
+
} catch (err) {
|
|
1614
|
+
res.status(500).json({ success: false, error: `\u4FDD\u5B58\u5931\u8D25: ${String(err)}` });
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
app2.get("/api/files/snapshot", (req, res) => {
|
|
1618
|
+
const root = validateRoot(req.query.root);
|
|
1619
|
+
if (!root) {
|
|
1620
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const files = [];
|
|
1624
|
+
const walk = (dir, depth) => {
|
|
1625
|
+
if (depth > 6 || files.length > 8e3) return;
|
|
1626
|
+
let entries;
|
|
1627
|
+
try {
|
|
1628
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1629
|
+
} catch {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
for (const ent of entries) {
|
|
1633
|
+
if (FILE_TREE_EXCLUDES.has(ent.name) || ent.name.startsWith(".")) continue;
|
|
1634
|
+
const full = join5(dir, ent.name);
|
|
1635
|
+
if (ent.isDirectory()) walk(full, depth + 1);
|
|
1636
|
+
else if (ent.isFile()) {
|
|
1637
|
+
try {
|
|
1638
|
+
const st = statSync2(full);
|
|
1639
|
+
files.push({ path: full, mtime: st.mtimeMs, size: st.size });
|
|
1640
|
+
} catch {
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
walk(root, 0);
|
|
1646
|
+
res.json({ success: true, data: { files } });
|
|
1647
|
+
});
|
|
1648
|
+
app2.get("/api/files/git-changes", (req, res) => {
|
|
1649
|
+
const root = validateRoot(req.query.root);
|
|
1650
|
+
if (!root) {
|
|
1651
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const isGit = gitRead(root, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
|
|
1655
|
+
if (!isGit) {
|
|
1656
|
+
res.json({ success: true, data: { git: false, files: [] } });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const prefix = (gitRead(root, ["rev-parse", "--show-prefix"]) || "").trim().replace(/\/$/, "");
|
|
1660
|
+
const porcelain = gitRead(root, ["status", "--porcelain"]) || "";
|
|
1661
|
+
const numstatOut = gitRead(root, ["diff", "--numstat", "HEAD"]) || "";
|
|
1662
|
+
const numstat = /* @__PURE__ */ new Map();
|
|
1663
|
+
for (const line of numstatOut.split("\n")) {
|
|
1664
|
+
if (!line.trim()) continue;
|
|
1665
|
+
const [a, d, ...rest] = line.split(" ");
|
|
1666
|
+
numstat.set(rest.join(" "), { additions: a === "-" ? 0 : parseInt(a, 10) || 0, deletions: d === "-" ? 0 : parseInt(d, 10) || 0 });
|
|
1667
|
+
}
|
|
1668
|
+
const files = [];
|
|
1669
|
+
for (const line of porcelain.split("\n")) {
|
|
1670
|
+
if (!line.trim()) continue;
|
|
1671
|
+
const xy = line.slice(0, 2);
|
|
1672
|
+
let rel = line.slice(3).trim();
|
|
1673
|
+
if (rel.includes(" -> ")) rel = rel.split(" -> ").pop().trim();
|
|
1674
|
+
if (rel.startsWith('"') && rel.endsWith('"')) {
|
|
1675
|
+
try {
|
|
1676
|
+
rel = JSON.parse(rel);
|
|
1677
|
+
} catch {
|
|
1678
|
+
rel = rel.slice(1, -1);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
let displayPath = rel;
|
|
1682
|
+
if (prefix) {
|
|
1683
|
+
if (!rel.startsWith(prefix + "/")) continue;
|
|
1684
|
+
displayPath = rel.slice(prefix.length + 1);
|
|
1685
|
+
}
|
|
1686
|
+
const status = parsePorcelainStatus(xy);
|
|
1687
|
+
const ns = numstat.get(rel);
|
|
1688
|
+
let additions = ns?.additions ?? 0;
|
|
1689
|
+
const deletions = ns?.deletions ?? 0;
|
|
1690
|
+
if (!ns && status === "added") {
|
|
1691
|
+
try {
|
|
1692
|
+
const buf = readFileSync3(join5(root, displayPath));
|
|
1693
|
+
if (!buf.includes(0)) additions = countLines(buf.toString("utf-8"));
|
|
1694
|
+
} catch {
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
files.push({ path: join5(root, displayPath), displayPath, status, additions, deletions });
|
|
1698
|
+
}
|
|
1699
|
+
res.json({ success: true, data: { git: true, files } });
|
|
1700
|
+
});
|
|
1701
|
+
app2.get("/api/files/diff", (req, res) => {
|
|
1702
|
+
const root = validateRoot(req.query.root);
|
|
1703
|
+
if (!root) {
|
|
1704
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
const filePath = typeof req.query.path === "string" ? req.query.path : "";
|
|
1708
|
+
const safePath = resolveWithinRoot(root, filePath);
|
|
1709
|
+
if (!safePath) {
|
|
1710
|
+
res.status(403).json({ success: false, error: "\u8DEF\u5F84\u8D8A\u754C\uFF1A\u4EC5\u5141\u8BB8\u8BBF\u95EE\u9879\u76EE\u76EE\u5F55\u5185\u7684\u6587\u4EF6" });
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
let modified = "";
|
|
1714
|
+
try {
|
|
1715
|
+
const st = statSync2(safePath);
|
|
1716
|
+
if (st.isFile() && st.size <= FILE_READ_LIMIT) {
|
|
1717
|
+
const buf = readFileSync3(safePath);
|
|
1718
|
+
if (!buf.includes(0)) modified = buf.toString("utf-8");
|
|
1719
|
+
}
|
|
1720
|
+
} catch {
|
|
1721
|
+
}
|
|
1722
|
+
let original = "";
|
|
1723
|
+
let status = "added";
|
|
1724
|
+
const rel = relToGitRepo(root, safePath);
|
|
1725
|
+
if (rel) {
|
|
1726
|
+
status = parsePorcelainStatus((gitRead(root, ["status", "--porcelain", "--", safePath]) || "").slice(0, 2));
|
|
1727
|
+
if (status !== "added") {
|
|
1728
|
+
const head = gitRead(root, ["show", `HEAD:${rel}`]);
|
|
1729
|
+
if (head !== null) original = head;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
res.json({ success: true, data: { path: safePath, original, modified, status } });
|
|
1733
|
+
});
|
|
1734
|
+
function isValidBranchName(root, branch) {
|
|
1735
|
+
if (!branch || branch.length > 120) return false;
|
|
1736
|
+
return gitRead(root, ["check-ref-format", "--branch", branch]) !== null;
|
|
1737
|
+
}
|
|
1738
|
+
app2.get("/api/files/git-info", (req, res) => {
|
|
1739
|
+
const root = validateRoot(req.query.root);
|
|
1740
|
+
if (!root) {
|
|
1741
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
const isGit = gitRead(root, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
|
|
1745
|
+
if (!isGit) {
|
|
1746
|
+
res.json({ success: true, data: { isGit: false } });
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
const branch = (gitRead(root, ["rev-parse", "--abbrev-ref", "HEAD"]) || "").trim();
|
|
1750
|
+
const statusOut = gitRead(root, ["status", "--porcelain"]) || "";
|
|
1751
|
+
const changes = statusOut.split("\n").map((l) => l.trimEnd()).filter(Boolean).map((l) => l.slice(3).trim());
|
|
1752
|
+
res.json({
|
|
1753
|
+
success: true,
|
|
1754
|
+
data: {
|
|
1755
|
+
isGit: true,
|
|
1756
|
+
branch: branch || "(\u672A\u77E5)",
|
|
1757
|
+
hasChanges: changes.length > 0,
|
|
1758
|
+
changeCount: changes.length,
|
|
1759
|
+
// 最多返回前 50 个变更文件(卡片展示用),完整数量以 changeCount 为准
|
|
1760
|
+
changes: changes.slice(0, 50)
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
});
|
|
1764
|
+
app2.post("/api/files/git-checkout", (req, res) => {
|
|
1765
|
+
const { root: rawRoot, branch } = req.body;
|
|
1766
|
+
const root = validateRoot(rawRoot);
|
|
1767
|
+
if (!root) {
|
|
1768
|
+
res.status(400).json({ success: false, error: "root \u5FC5\u987B\u4E3A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55" });
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (typeof branch !== "string" || !isValidBranchName(root, branch.trim())) {
|
|
1772
|
+
res.status(400).json({ success: false, error: `\u5206\u652F\u540D\u4E0D\u5408\u6CD5: ${branch || ""}` });
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
const name = branch.trim();
|
|
1776
|
+
const isGit = gitRead(root, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
|
|
1777
|
+
if (!isGit) {
|
|
1778
|
+
res.status(400).json({ success: false, error: "\u76EE\u6807\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93" });
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
const exists = gitRead(root, ["rev-parse", "--verify", `refs/heads/${name}`]) !== null;
|
|
1782
|
+
const out = gitRead(root, exists ? ["checkout", name] : ["checkout", "-b", name]);
|
|
1783
|
+
if (out === null) {
|
|
1784
|
+
res.status(500).json({ success: false, error: `\u5207\u6362/\u521B\u5EFA\u5206\u652F\u5931\u8D25: ${name}` });
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
const current = (gitRead(root, ["rev-parse", "--abbrev-ref", "HEAD"]) || "").trim();
|
|
1788
|
+
res.json({ success: true, data: { branch: current, created: !exists } });
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// server/routes/store.ts
|
|
1793
|
+
function registerStoreRoutes(app2) {
|
|
1794
|
+
function storeScope(req, res) {
|
|
1795
|
+
const src = { ...req.query || {}, ...req.body || {} };
|
|
1796
|
+
const uid = typeof src.uid === "string" ? src.uid.trim() : "";
|
|
1797
|
+
const clientId = typeof src.clientId === "string" ? src.clientId.trim() : "";
|
|
1798
|
+
if (!uid || !clientId) {
|
|
1799
|
+
res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId" });
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
return { uid, clientId };
|
|
1803
|
+
}
|
|
1804
|
+
app2.get("/api/store/user", (req, res) => {
|
|
1805
|
+
const uid = typeof req.query.uid === "string" ? req.query.uid.trim() : "";
|
|
1806
|
+
if (!uid) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid" });
|
|
1807
|
+
res.json({ success: true, data: getUser(uid) });
|
|
1808
|
+
});
|
|
1809
|
+
app2.put("/api/store/user", (req, res) => {
|
|
1810
|
+
const { uid, email, activeClientId } = req.body || {};
|
|
1811
|
+
if (!uid || typeof uid !== "string") return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid" });
|
|
1812
|
+
res.json({ success: true, data: upsertUser(uid, { email, activeClientId }) });
|
|
1813
|
+
});
|
|
1814
|
+
app2.get("/api/store/app", (req, res) => {
|
|
1815
|
+
const scope = storeScope(req, res);
|
|
1816
|
+
if (!scope) return;
|
|
1817
|
+
ensureNodes(scope.uid, scope.clientId);
|
|
1818
|
+
res.json({ success: true, data: { app: getApp(scope.uid, scope.clientId), nodes: getNodes(scope.uid, scope.clientId) } });
|
|
1819
|
+
});
|
|
1820
|
+
app2.put("/api/store/app/ui-state", (req, res) => {
|
|
1821
|
+
const { uid, clientId, uiState } = req.body || {};
|
|
1822
|
+
if (!uid || !clientId) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId" });
|
|
1823
|
+
setUiState(uid, clientId, uiState || {});
|
|
1824
|
+
res.json({ success: true });
|
|
1825
|
+
});
|
|
1826
|
+
app2.delete("/api/store/app", (req, res) => {
|
|
1827
|
+
const scope = storeScope(req, res);
|
|
1828
|
+
if (!scope) return;
|
|
1829
|
+
deleteApp(scope.uid, scope.clientId);
|
|
1830
|
+
res.json({ success: true, data: getUser(scope.uid) });
|
|
1831
|
+
});
|
|
1832
|
+
app2.post("/api/store/apps/sync", async (req, res) => {
|
|
1833
|
+
const { uid, clientId, overview } = req.body || {};
|
|
1834
|
+
if (!uid || !clientId) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId" });
|
|
1835
|
+
const ov = overview || {};
|
|
1836
|
+
const state = String(ov?.appInfo?.state || ov?.appInfo?.status || "").toUpperCase();
|
|
1837
|
+
let cases;
|
|
1838
|
+
if (state === "TESTING") {
|
|
1839
|
+
try {
|
|
1840
|
+
const r = await runWfDeveloperForApp(["app", "testcase", "list", "--client-id", clientId], clientId);
|
|
1841
|
+
const d = r?.data;
|
|
1842
|
+
cases = Array.isArray(d) ? d : Array.isArray(d?.cases) ? d.cases : Array.isArray(d?.list) ? d.list : [];
|
|
1843
|
+
} catch {
|
|
1844
|
+
cases = [];
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
res.json({ success: true, data: syncApp(uid, clientId, ov, { cases }) });
|
|
1848
|
+
});
|
|
1849
|
+
app2.post("/api/store/apps/migrate", (req, res) => {
|
|
1850
|
+
const { uid, clientId, appName, steps, executor } = req.body || {};
|
|
1851
|
+
if (!uid || !clientId) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId" });
|
|
1852
|
+
res.json({ success: true, data: migrateDraft(uid, clientId, appName || "", steps || {}, typeof executor === "string" ? executor : void 0) });
|
|
1853
|
+
});
|
|
1854
|
+
app2.get("/api/store/node", (req, res) => {
|
|
1855
|
+
const scope = storeScope(req, res);
|
|
1856
|
+
if (!scope) return;
|
|
1857
|
+
const node = typeof req.query.node === "string" ? req.query.node : "";
|
|
1858
|
+
if (!node) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 node" });
|
|
1859
|
+
ensureNodes(scope.uid, scope.clientId);
|
|
1860
|
+
res.json({ success: true, data: { node: getNode(scope.uid, scope.clientId, node), items: loadMessages(scope.uid, scope.clientId, node) } });
|
|
1861
|
+
});
|
|
1862
|
+
app2.put("/api/store/node", (req, res) => {
|
|
1863
|
+
const { uid, clientId, node, status, sessionId, keyDataPatch, keyDataReplace } = req.body || {};
|
|
1864
|
+
if (!uid || !clientId || !node) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId / node" });
|
|
1865
|
+
ensureNodes(uid, clientId);
|
|
1866
|
+
updateNode(uid, clientId, node, { status, sessionId, keyDataPatch, keyDataReplace });
|
|
1867
|
+
res.json({ success: true, data: getNode(uid, clientId, node) });
|
|
1868
|
+
});
|
|
1869
|
+
app2.put("/api/store/node/messages", (req, res) => {
|
|
1870
|
+
const { uid, clientId, node, items } = req.body || {};
|
|
1871
|
+
if (!uid || !clientId || !node) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId / node" });
|
|
1872
|
+
ensureNodes(uid, clientId);
|
|
1873
|
+
upsertMessages(uid, clientId, node, Array.isArray(items) ? items : []);
|
|
1874
|
+
res.json({ success: true });
|
|
1875
|
+
});
|
|
1876
|
+
app2.put("/api/store/nodes/status", (req, res) => {
|
|
1877
|
+
const { uid, clientId, statusMap } = req.body || {};
|
|
1878
|
+
if (!uid || !clientId || !statusMap || typeof statusMap !== "object") return res.status(400).json({ success: false, error: "\u7F3A\u5C11\u53C2\u6570" });
|
|
1879
|
+
ensureNodes(uid, clientId);
|
|
1880
|
+
setNodesStatus(uid, clientId, statusMap);
|
|
1881
|
+
res.json({ success: true });
|
|
1882
|
+
});
|
|
1883
|
+
app2.get("/api/store/settings", (_req, res) => {
|
|
1884
|
+
res.json({ success: true, data: getSettings() });
|
|
1885
|
+
});
|
|
1886
|
+
app2.put("/api/store/settings", (req, res) => {
|
|
1887
|
+
const { cliPath, version } = req.body || {};
|
|
1888
|
+
res.json({ success: true, data: setSettings({ cliPath, version }) });
|
|
1889
|
+
});
|
|
1890
|
+
app2.put("/api/store/app/executor", (req, res) => {
|
|
1891
|
+
const { uid, clientId, executor } = req.body || {};
|
|
1892
|
+
if (!uid || !clientId) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId" });
|
|
1893
|
+
res.json({ success: true, data: setAppExecutor(uid, clientId, executor || "") });
|
|
1894
|
+
});
|
|
1895
|
+
app2.put("/api/store/app/project-dir", (req, res) => {
|
|
1896
|
+
const { uid, clientId, projectDir } = req.body || {};
|
|
1897
|
+
if (!uid || !clientId) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId" });
|
|
1898
|
+
setAppProjectDir(uid, clientId, typeof projectDir === "string" ? projectDir : "");
|
|
1899
|
+
res.json({ success: true });
|
|
1900
|
+
});
|
|
1901
|
+
app2.put("/api/store/app/current-node", (req, res) => {
|
|
1902
|
+
const { uid, clientId, node } = req.body || {};
|
|
1903
|
+
if (!uid || !clientId) return res.status(400).json({ success: false, error: "\u7F3A\u5C11 uid / clientId" });
|
|
1904
|
+
setCurrentNode(uid, clientId, node || "");
|
|
1905
|
+
res.json({ success: true });
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// server/routes/webhookProxy.ts
|
|
1910
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1911
|
+
|
|
1912
|
+
// server/routes/webhookVerify.ts
|
|
1913
|
+
function verifyForward(status, responseBody) {
|
|
1914
|
+
if (status !== 200) return { verified: false, detail: `HTTP ${status}\uFF08\u9700 200\uFF09` };
|
|
1915
|
+
const trimmed = (responseBody || "").trim();
|
|
1916
|
+
if (!trimmed) return { verified: false, detail: "\u54CD\u5E94\u4F53\u4E3A\u7A7A\uFF08\u9700 result.resultStatus=S\uFF09" };
|
|
1917
|
+
let parsed;
|
|
1918
|
+
try {
|
|
1919
|
+
parsed = JSON.parse(trimmed);
|
|
1920
|
+
} catch {
|
|
1921
|
+
return { verified: false, detail: "\u54CD\u5E94\u4F53\u975E\u5408\u6CD5 JSON" };
|
|
1922
|
+
}
|
|
1923
|
+
const result = parsed && typeof parsed === "object" ? parsed.result : void 0;
|
|
1924
|
+
if (!result || typeof result !== "object") return { verified: false, detail: "\u54CD\u5E94\u4F53\u7F3A\u5C11 result \u5B57\u6BB5" };
|
|
1925
|
+
const rs = result.resultStatus;
|
|
1926
|
+
const rc = result.resultCode;
|
|
1927
|
+
if (rs !== "S") return { verified: false, detail: `resultStatus=${rs ?? "(\u7A7A)"}\uFF08\u9700 S\uFF09` };
|
|
1928
|
+
if (rc !== "SUCCESS") return { verified: false, detail: `resultCode=${rc ?? "(\u7A7A)"}\uFF08\u9700 SUCCESS\uFF09` };
|
|
1929
|
+
return { verified: true, detail: "resultStatus=S \xB7 resultCode=SUCCESS \xB7 HTTP 200" };
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// server/routes/webhookProxy.ts
|
|
1933
|
+
var MAX_RECORDS = 200;
|
|
1934
|
+
function registerWebhookProxyRoutes(app2) {
|
|
1935
|
+
let state = { child: null, running: false, config: null, startedAt: 0, records: [] };
|
|
1936
|
+
let seq = 0;
|
|
1937
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
1938
|
+
function broadcast(event, data) {
|
|
1939
|
+
const payload = `event: ${event}
|
|
1940
|
+
data: ${JSON.stringify(data)}
|
|
1941
|
+
|
|
1942
|
+
`;
|
|
1943
|
+
for (const client of sseClients) {
|
|
1944
|
+
try {
|
|
1945
|
+
client.write(payload);
|
|
1946
|
+
} catch {
|
|
1947
|
+
sseClients.delete(client);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function killChild() {
|
|
1952
|
+
if (state.child) {
|
|
1953
|
+
try {
|
|
1954
|
+
state.child.kill("SIGTERM");
|
|
1955
|
+
} catch {
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
process.on("exit", killChild);
|
|
1960
|
+
const onSignal = (sig) => {
|
|
1961
|
+
killChild();
|
|
1962
|
+
process.removeListener(sig, onSignal);
|
|
1963
|
+
process.kill(process.pid, sig);
|
|
1964
|
+
};
|
|
1965
|
+
process.on("SIGINT", onSignal);
|
|
1966
|
+
process.on("SIGTERM", onSignal);
|
|
1967
|
+
function handleStdoutLine(line) {
|
|
1968
|
+
let parsed;
|
|
1969
|
+
try {
|
|
1970
|
+
parsed = JSON.parse(line);
|
|
1971
|
+
} catch {
|
|
1972
|
+
broadcast("log", { text: line, time: Date.now() });
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
if (parsed && parsed.ok === false && parsed.error) {
|
|
1976
|
+
const msg = parsed.error?.message || parsed.error?.code || "CLI \u9519\u8BEF";
|
|
1977
|
+
broadcast("log", { text: `\u2717 ${msg}`, level: "err", time: Date.now() });
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
if (!parsed || parsed.type !== "forward") {
|
|
1981
|
+
broadcast("log", { text: line, time: Date.now() });
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (parsed.skipped) return;
|
|
1985
|
+
const status = Number(parsed.status) || 0;
|
|
1986
|
+
const responseBody = typeof parsed.responseBody === "string" ? parsed.responseBody : "";
|
|
1987
|
+
const { verified, detail } = verifyForward(status, responseBody);
|
|
1988
|
+
const rec = {
|
|
1989
|
+
id: `rec-${++seq}`,
|
|
1990
|
+
requestId: String(parsed.requestId || ""),
|
|
1991
|
+
eventType: String(parsed.eventType || ""),
|
|
1992
|
+
method: String(parsed.method || ""),
|
|
1993
|
+
uri: String(parsed.uri || ""),
|
|
1994
|
+
target: String(parsed.target || ""),
|
|
1995
|
+
status,
|
|
1996
|
+
elapsedMs: Number(parsed.elapsedMs) || 0,
|
|
1997
|
+
requestBody: typeof parsed.requestBody === "string" ? parsed.requestBody : "",
|
|
1998
|
+
responseBody,
|
|
1999
|
+
error: String(parsed.error || ""),
|
|
2000
|
+
verified,
|
|
2001
|
+
verifyDetail: parsed.error ? String(parsed.error) : detail,
|
|
2002
|
+
time: Number(parsed.time) || Date.now()
|
|
2003
|
+
};
|
|
2004
|
+
state.records.push(rec);
|
|
2005
|
+
if (state.records.length > MAX_RECORDS) state.records.splice(0, state.records.length - MAX_RECORDS);
|
|
2006
|
+
broadcast("record", { record: rec, time: Date.now() });
|
|
2007
|
+
}
|
|
2008
|
+
app2.get("/api/webhooks/proxy/status", (_req, res) => {
|
|
2009
|
+
res.json({
|
|
2010
|
+
success: true,
|
|
2011
|
+
data: {
|
|
2012
|
+
running: state.running,
|
|
2013
|
+
config: state.config,
|
|
2014
|
+
startedAt: state.startedAt || null,
|
|
2015
|
+
records: state.records
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
});
|
|
2019
|
+
app2.get("/api/webhooks/proxy/records", (_req, res) => {
|
|
2020
|
+
res.json({ success: true, data: { records: state.records } });
|
|
2021
|
+
});
|
|
2022
|
+
app2.post("/api/webhooks/proxy/start", async (req, res) => {
|
|
2023
|
+
const body = req.body || {};
|
|
2024
|
+
if (state.running) {
|
|
2025
|
+
res.json({
|
|
2026
|
+
success: true,
|
|
2027
|
+
message: "\u8F6C\u53D1\u5DF2\u5728\u8FD0\u884C\u4E2D",
|
|
2028
|
+
data: { running: true, config: state.config, records: state.records }
|
|
2029
|
+
});
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
const appId = typeof body.appId === "string" ? body.appId.trim() : "";
|
|
2033
|
+
const forwardTo = typeof body.forwardTo === "string" ? body.forwardTo.trim() : "";
|
|
2034
|
+
if (!appId) {
|
|
2035
|
+
res.status(400).json({ success: false, error: "\u7F3A\u5C11 appId" });
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
if (!forwardTo) {
|
|
2039
|
+
res.status(400).json({ success: false, error: "\u7F3A\u5C11 forwardTo\uFF08\u672C\u5730\u63A5\u6536\u7AEF\u70B9 URL\uFF09" });
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (!/^https?:\/\//i.test(forwardTo)) {
|
|
2043
|
+
res.status(400).json({ success: false, error: "forwardTo \u5FC5\u987B\u4E3A http/https URL" });
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const events = Array.isArray(body.events) ? body.events.map((e) => String(e).trim()).filter(Boolean) : [];
|
|
2047
|
+
const headers = [];
|
|
2048
|
+
if (Array.isArray(body.headers)) {
|
|
2049
|
+
for (const h of body.headers) {
|
|
2050
|
+
if (h && typeof h === "object") {
|
|
2051
|
+
const key = String(h.key ?? "").trim();
|
|
2052
|
+
const value = String(h.value ?? "");
|
|
2053
|
+
if (key) headers.push({ key, value });
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
} else if (body.headers && typeof body.headers === "object") {
|
|
2057
|
+
for (const [key, value] of Object.entries(body.headers)) {
|
|
2058
|
+
if (key.trim()) headers.push({ key: key.trim(), value: String(value ?? "") });
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
const bin = findWfDeveloper();
|
|
2062
|
+
if (!bin) {
|
|
2063
|
+
res.status(503).json({ success: false, error: "\u672A\u627E\u5230 wf-developer CLI" });
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
try {
|
|
2067
|
+
await ensureFreshCredentials(bin);
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
res.status(401).json({ success: false, error: `\u6388\u6743\u5DF2\u5931\u6548: ${err instanceof Error ? err.message : String(err)}` });
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
const sel = ensureActiveApp(bin, appId);
|
|
2073
|
+
if (!sel.ok) {
|
|
2074
|
+
res.status(503).json({ success: false, error: `\u9009\u62E9\u5E94\u7528\u5931\u8D25 (wf-developer app use ${appId}): ${sel.error}` });
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const args = [...baseUrlArgs(), "app", "listen", "--app-id", appId, "--forward-to", forwardTo, "--json"];
|
|
2078
|
+
if (events.length) args.push("--events", events.join(","));
|
|
2079
|
+
for (const h of headers) args.push("--headers", `${h.key}:${h.value}`);
|
|
2080
|
+
const config = { appId, forwardTo, events, headers };
|
|
2081
|
+
console.log(`[webhook-proxy] Starting: wf-developer ${args.join(" ")}`);
|
|
2082
|
+
const child = spawn2(bin, args, { env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
|
|
2083
|
+
state = { child, running: true, config, startedAt: Date.now(), records: [] };
|
|
2084
|
+
seq = 0;
|
|
2085
|
+
let buffer = "";
|
|
2086
|
+
child.stdout?.on("data", (chunk) => {
|
|
2087
|
+
buffer += chunk.toString();
|
|
2088
|
+
let nl;
|
|
2089
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
2090
|
+
const line = buffer.slice(0, nl).trim();
|
|
2091
|
+
buffer = buffer.slice(nl + 1);
|
|
2092
|
+
if (line) handleStdoutLine(line);
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
child.stderr?.on("data", (chunk) => {
|
|
2096
|
+
broadcast("log", { text: chunk.toString(), time: Date.now(), level: "stderr" });
|
|
2097
|
+
});
|
|
2098
|
+
child.on("close", (code) => {
|
|
2099
|
+
console.log(`[webhook-proxy] Process exited with code ${code}`);
|
|
2100
|
+
broadcast("status", { running: false, exitCode: code, time: Date.now() });
|
|
2101
|
+
state.child = null;
|
|
2102
|
+
state.running = false;
|
|
2103
|
+
});
|
|
2104
|
+
child.on("error", (err) => {
|
|
2105
|
+
console.error(`[webhook-proxy] Process error: ${err.message}`);
|
|
2106
|
+
broadcast("error", { message: err.message, time: Date.now() });
|
|
2107
|
+
state.child = null;
|
|
2108
|
+
state.running = false;
|
|
2109
|
+
});
|
|
2110
|
+
broadcast("status", { running: true, config, time: Date.now() });
|
|
2111
|
+
res.json({
|
|
2112
|
+
success: true,
|
|
2113
|
+
message: "\u8F6C\u53D1\u5DF2\u542F\u52A8",
|
|
2114
|
+
data: { running: true, config, startedAt: state.startedAt, records: state.records }
|
|
2115
|
+
});
|
|
2116
|
+
});
|
|
2117
|
+
app2.post("/api/webhooks/proxy/stop", (_req, res) => {
|
|
2118
|
+
if (!state.running || !state.child) {
|
|
2119
|
+
res.json({ success: true, message: "\u8F6C\u53D1\u672A\u5728\u8FD0\u884C" });
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
const child = state.child;
|
|
2123
|
+
try {
|
|
2124
|
+
child.kill("SIGTERM");
|
|
2125
|
+
setTimeout(() => {
|
|
2126
|
+
try {
|
|
2127
|
+
child.kill("SIGKILL");
|
|
2128
|
+
} catch {
|
|
2129
|
+
}
|
|
2130
|
+
}, 3e3);
|
|
2131
|
+
} catch (err) {
|
|
2132
|
+
console.error(`[webhook-proxy] Failed to kill process: ${err instanceof Error ? err.message : String(err)}`);
|
|
2133
|
+
}
|
|
2134
|
+
state.child = null;
|
|
2135
|
+
state.running = false;
|
|
2136
|
+
broadcast("status", { running: false, stopped: true, config: state.config, time: Date.now() });
|
|
2137
|
+
res.json({ success: true, message: "\u8F6C\u53D1\u5DF2\u505C\u6B62" });
|
|
2138
|
+
});
|
|
2139
|
+
app2.post("/api/webhooks/proxy/records/clear", (_req, res) => {
|
|
2140
|
+
state.records = [];
|
|
2141
|
+
broadcast("records_cleared", { time: Date.now() });
|
|
2142
|
+
res.json({ success: true });
|
|
2143
|
+
});
|
|
2144
|
+
app2.get("/api/webhooks/proxy/stream", (req, res) => {
|
|
2145
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
2146
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
2147
|
+
res.setHeader("Connection", "keep-alive");
|
|
2148
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
2149
|
+
res.flushHeaders?.();
|
|
2150
|
+
res.write(`event: status
|
|
2151
|
+
data: ${JSON.stringify({
|
|
2152
|
+
running: state.running,
|
|
2153
|
+
config: state.config,
|
|
2154
|
+
startedAt: state.startedAt || null,
|
|
2155
|
+
time: Date.now()
|
|
2156
|
+
})}
|
|
2157
|
+
|
|
2158
|
+
`);
|
|
2159
|
+
res.write(`event: snapshot
|
|
2160
|
+
data: ${JSON.stringify({ records: state.records, time: Date.now() })}
|
|
2161
|
+
|
|
2162
|
+
`);
|
|
2163
|
+
sseClients.add(res);
|
|
2164
|
+
const heartbeat = setInterval(() => {
|
|
2165
|
+
try {
|
|
2166
|
+
res.write(`:heartbeat
|
|
2167
|
+
|
|
2168
|
+
`);
|
|
2169
|
+
} catch {
|
|
2170
|
+
clearInterval(heartbeat);
|
|
2171
|
+
}
|
|
2172
|
+
}, 3e4);
|
|
2173
|
+
req.on("close", () => {
|
|
2174
|
+
clearInterval(heartbeat);
|
|
2175
|
+
sseClients.delete(res);
|
|
2176
|
+
});
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// server/routes/logsTail.ts
|
|
2181
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2182
|
+
function normalizeRecord(raw) {
|
|
2183
|
+
let obj = raw;
|
|
2184
|
+
if (!obj || typeof obj !== "object") return null;
|
|
2185
|
+
for (let i = 0; i < 4; i++) {
|
|
2186
|
+
if (Array.isArray(obj)) {
|
|
2187
|
+
obj = obj[0];
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
if (obj.requestPath !== void 0 || obj.path !== void 0 || obj.requestMethod !== void 0 || obj.method !== void 0) break;
|
|
2191
|
+
const nest = obj.data ?? obj.result ?? obj.record ?? obj.payload;
|
|
2192
|
+
if (nest && typeof nest === "object") {
|
|
2193
|
+
obj = nest;
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return null;
|
|
2199
|
+
const str = (v) => v === void 0 || v === null ? void 0 : typeof v === "string" ? v : JSON.stringify(v);
|
|
2200
|
+
let requestPath = str(obj.requestPath ?? obj.path ?? obj.uri ?? obj.url);
|
|
2201
|
+
let queryString = str(obj.queryString ?? obj.query ?? obj.params);
|
|
2202
|
+
if (requestPath) {
|
|
2203
|
+
const hm = requestPath.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]+(.*?)$/i);
|
|
2204
|
+
if (hm) requestPath = hm[1] || "/";
|
|
2205
|
+
const q = requestPath.indexOf("?");
|
|
2206
|
+
if (q !== -1 && !queryString) {
|
|
2207
|
+
queryString = requestPath.slice(q + 1);
|
|
2208
|
+
requestPath = requestPath.slice(0, q);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
const rec = {
|
|
2212
|
+
type: "tail",
|
|
2213
|
+
requestMethod: str(obj.requestMethod ?? obj.method ?? obj.httpMethod),
|
|
2214
|
+
requestPath,
|
|
2215
|
+
queryString,
|
|
2216
|
+
requestBody: str(obj.requestBody ?? obj.reqBody ?? obj.request ?? obj.body),
|
|
2217
|
+
requestHeaders: formatHeaders(obj.requestHeaders ?? obj.reqHeaders),
|
|
2218
|
+
responseHeaders: formatHeaders(obj.responseHeaders ?? obj.respHeaders),
|
|
2219
|
+
httpStatus: obj.httpStatus ?? obj.status ?? obj.statusCode ?? obj.responseCode,
|
|
2220
|
+
responseBody: str(obj.responseBody ?? obj.respBody ?? obj.response),
|
|
2221
|
+
traceId: str(obj.traceId ?? obj.trace_id ?? obj.requestId),
|
|
2222
|
+
timestamp: obj.timestamp ?? obj.time,
|
|
2223
|
+
logTime: str(obj.logTime),
|
|
2224
|
+
clientId: str(obj.clientId ?? obj.appId)
|
|
2225
|
+
};
|
|
2226
|
+
if (!rec.requestPath && !rec.requestBody && !rec.responseBody) return null;
|
|
2227
|
+
return rec;
|
|
2228
|
+
}
|
|
2229
|
+
function formatHeaders(v) {
|
|
2230
|
+
if (v === void 0 || v === null) return void 0;
|
|
2231
|
+
if (typeof v === "string") return v;
|
|
2232
|
+
if (Array.isArray(v)) {
|
|
2233
|
+
const lines = v.map((it) => {
|
|
2234
|
+
if (it && typeof it === "object") return `${it.name ?? it.key ?? ""}: ${it.value ?? ""}`;
|
|
2235
|
+
return String(it);
|
|
2236
|
+
});
|
|
2237
|
+
return lines.join("\n");
|
|
2238
|
+
}
|
|
2239
|
+
if (typeof v === "object") {
|
|
2240
|
+
return Object.entries(v).map(([k, val]) => `${k}: ${val}`).join("\n");
|
|
2241
|
+
}
|
|
2242
|
+
return String(v);
|
|
2243
|
+
}
|
|
2244
|
+
function describeCliJson(obj) {
|
|
2245
|
+
if (!obj || typeof obj !== "object") return null;
|
|
2246
|
+
if (obj.type === "status") {
|
|
2247
|
+
switch (obj.event) {
|
|
2248
|
+
case "connected":
|
|
2249
|
+
return { level: "info", code: "connected", text: `\u5DF2\u8FDE\u63A5\u65E5\u5FD7\u6D41${obj.app ? `\uFF08app=${obj.app}\uFF09` : ""}` };
|
|
2250
|
+
case "connection_lost":
|
|
2251
|
+
return { level: "warn", code: "connection_lost", text: `\u8FDE\u63A5\u65AD\u5F00\uFF1A${obj.error || "\u672A\u77E5\u9519\u8BEF"}${obj.retryInSeconds ? `\uFF0C${obj.retryInSeconds}s \u540E\u91CD\u8BD5` : ""}` };
|
|
2252
|
+
case "token_rejected":
|
|
2253
|
+
return { level: "warn", code: "token_rejected", text: "\u8BBF\u95EE token \u88AB\u62D2\u7EDD\uFF0C\u6B63\u5728\u5C1D\u8BD5\u5237\u65B0\u2026" };
|
|
2254
|
+
case "heartbeat_failed":
|
|
2255
|
+
return { level: "warn", code: "heartbeat_failed", text: `\u5FC3\u8DF3\u5931\u8D25\uFF1A${obj.error || "\u672A\u77E5\u9519\u8BEF"}` };
|
|
2256
|
+
case "interrupted":
|
|
2257
|
+
return { level: "info", code: "interrupted", text: "\u76D1\u542C\u88AB\u4E2D\u65AD" };
|
|
2258
|
+
default:
|
|
2259
|
+
return { level: "info", code: "status", text: `status: ${obj.event || "unknown"}` };
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
if (obj.ok === false && obj.error) {
|
|
2263
|
+
const code = obj.error.code || "INTERNAL_ERROR";
|
|
2264
|
+
const msg = obj.error.message || "";
|
|
2265
|
+
if (code === "AUTH_REQUIRED") return { level: "err", code, text: `\u672A\u767B\u5F55\uFF1A\u8BF7\u5148\u6267\u884C wf-developer login\uFF08${msg}\uFF09` };
|
|
2266
|
+
return { level: "err", code, text: `${code}: ${msg}` };
|
|
2267
|
+
}
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
function registerLogsTailRoutes(app2) {
|
|
2271
|
+
let tail = { child: null, running: false, appId: "", startedAt: 0, argsKey: "" };
|
|
2272
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
2273
|
+
function killChild() {
|
|
2274
|
+
if (tail.child) {
|
|
2275
|
+
try {
|
|
2276
|
+
tail.child.kill("SIGTERM");
|
|
2277
|
+
} catch {
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
let cleanupRegistered = false;
|
|
2282
|
+
if (!cleanupRegistered) {
|
|
2283
|
+
cleanupRegistered = true;
|
|
2284
|
+
process.on("exit", killChild);
|
|
2285
|
+
const onSignal = (sig) => {
|
|
2286
|
+
killChild();
|
|
2287
|
+
process.removeListener(sig, onSignal);
|
|
2288
|
+
process.kill(process.pid, sig);
|
|
2289
|
+
};
|
|
2290
|
+
process.on("SIGINT", onSignal);
|
|
2291
|
+
process.on("SIGTERM", onSignal);
|
|
2292
|
+
}
|
|
2293
|
+
function broadcast(event, data) {
|
|
2294
|
+
const payload = `event: ${event}
|
|
2295
|
+
data: ${JSON.stringify(data)}
|
|
2296
|
+
|
|
2297
|
+
`;
|
|
2298
|
+
for (const client of sseClients) {
|
|
2299
|
+
try {
|
|
2300
|
+
client.write(payload);
|
|
2301
|
+
} catch {
|
|
2302
|
+
sseClients.delete(client);
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
function resetState() {
|
|
2307
|
+
tail = { child: null, running: false, appId: "", startedAt: 0, argsKey: "" };
|
|
2308
|
+
}
|
|
2309
|
+
app2.get("/api/logs/tail/status", (_req, res) => {
|
|
2310
|
+
res.json({
|
|
2311
|
+
success: true,
|
|
2312
|
+
data: { running: tail.running, appId: tail.appId || null, startedAt: tail.startedAt || null }
|
|
2313
|
+
});
|
|
2314
|
+
});
|
|
2315
|
+
app2.post("/api/logs/tail/start", async (req, res) => {
|
|
2316
|
+
const { appId } = req.body || {};
|
|
2317
|
+
if (!appId || typeof appId !== "string") {
|
|
2318
|
+
res.status(400).json({ success: false, error: "\u7F3A\u5C11 appId" });
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
const args = [...baseUrlArgs(), "app", "logs", "tail", "--json", "--app-id", appId];
|
|
2322
|
+
const argsKey = args.join(" ");
|
|
2323
|
+
if (tail.running && tail.appId === appId && tail.argsKey === argsKey) {
|
|
2324
|
+
res.json({ success: true, message: "tail \u5DF2\u5728\u8FD0\u884C\u4E2D", data: { running: true, appId: tail.appId } });
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
if (tail.running && tail.child) {
|
|
2328
|
+
try {
|
|
2329
|
+
tail.child.kill("SIGTERM");
|
|
2330
|
+
} catch {
|
|
2331
|
+
}
|
|
2332
|
+
resetState();
|
|
2333
|
+
}
|
|
2334
|
+
const bin = findWfDeveloper();
|
|
2335
|
+
if (!bin) {
|
|
2336
|
+
res.status(503).json({ success: false, error: "\u672A\u627E\u5230 wf-developer CLI" });
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
try {
|
|
2340
|
+
await ensureFreshCredentials(bin);
|
|
2341
|
+
} catch (err) {
|
|
2342
|
+
res.status(401).json({ success: false, error: `\u6388\u6743\u5DF2\u5931\u6548: ${err instanceof Error ? err.message : String(err)}` });
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
const sel = ensureActiveApp(bin, appId);
|
|
2346
|
+
if (!sel.ok) {
|
|
2347
|
+
res.status(503).json({ success: false, error: `\u9009\u62E9\u5E94\u7528\u5931\u8D25 (wf-developer app use ${appId}): ${sel.error}` });
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
console.log(`[logs-tail] Starting: wf-developer ${argsKey}`);
|
|
2351
|
+
const child = spawn3(bin, args, { env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
|
|
2352
|
+
tail = { child, running: true, appId, startedAt: Date.now(), argsKey };
|
|
2353
|
+
let buffer = "";
|
|
2354
|
+
let dbg = 0;
|
|
2355
|
+
child.stdout?.on("data", (chunk) => {
|
|
2356
|
+
buffer += chunk.toString();
|
|
2357
|
+
let nl;
|
|
2358
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
2359
|
+
const line = buffer.slice(0, nl).trim();
|
|
2360
|
+
buffer = buffer.slice(nl + 1);
|
|
2361
|
+
if (!line) continue;
|
|
2362
|
+
if (dbg < 5) {
|
|
2363
|
+
dbg++;
|
|
2364
|
+
console.log(`[logs-tail] raw line: ${line.slice(0, 400)}`);
|
|
2365
|
+
}
|
|
2366
|
+
try {
|
|
2367
|
+
const parsed = JSON.parse(line);
|
|
2368
|
+
const rec = normalizeRecord(parsed);
|
|
2369
|
+
if (rec) {
|
|
2370
|
+
broadcast("record", { record: rec, time: Date.now() });
|
|
2371
|
+
} else {
|
|
2372
|
+
const desc = describeCliJson(parsed);
|
|
2373
|
+
if (desc) broadcast("log", { text: desc.text, level: desc.level, code: desc.code, time: Date.now() });
|
|
2374
|
+
}
|
|
2375
|
+
} catch {
|
|
2376
|
+
broadcast("log", { text: line, time: Date.now() });
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
child.stderr?.on("data", (chunk) => {
|
|
2381
|
+
broadcast("log", { text: chunk.toString(), time: Date.now(), level: "stderr" });
|
|
2382
|
+
});
|
|
2383
|
+
child.on("close", (code) => {
|
|
2384
|
+
console.log(`[logs-tail] Process exited with code ${code}`);
|
|
2385
|
+
broadcast("status", { running: false, exitCode: code, time: Date.now() });
|
|
2386
|
+
resetState();
|
|
2387
|
+
});
|
|
2388
|
+
child.on("error", (err) => {
|
|
2389
|
+
console.error(`[logs-tail] Process error: ${err.message}`);
|
|
2390
|
+
broadcast("error", { message: err.message, time: Date.now() });
|
|
2391
|
+
resetState();
|
|
2392
|
+
});
|
|
2393
|
+
broadcast("status", { running: true, appId, time: Date.now() });
|
|
2394
|
+
res.json({ success: true, message: "tail \u5DF2\u542F\u52A8", data: { running: true, appId, startedAt: tail.startedAt } });
|
|
2395
|
+
});
|
|
2396
|
+
app2.post("/api/logs/tail/stop", (_req, res) => {
|
|
2397
|
+
if (!tail.running || !tail.child) {
|
|
2398
|
+
res.json({ success: true, message: "tail \u672A\u5728\u8FD0\u884C" });
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
const child = tail.child;
|
|
2402
|
+
try {
|
|
2403
|
+
child.kill("SIGTERM");
|
|
2404
|
+
setTimeout(() => {
|
|
2405
|
+
try {
|
|
2406
|
+
child.kill("SIGKILL");
|
|
2407
|
+
} catch {
|
|
2408
|
+
}
|
|
2409
|
+
}, 3e3);
|
|
2410
|
+
} catch (err) {
|
|
2411
|
+
console.error(`[logs-tail] Failed to kill process: ${err instanceof Error ? err.message : String(err)}`);
|
|
2412
|
+
}
|
|
2413
|
+
resetState();
|
|
2414
|
+
broadcast("status", { running: false, stopped: true, time: Date.now() });
|
|
2415
|
+
res.json({ success: true, message: "tail \u5DF2\u505C\u6B62" });
|
|
2416
|
+
});
|
|
2417
|
+
app2.get("/api/logs/tail/stream", (req, res) => {
|
|
2418
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
2419
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
2420
|
+
res.setHeader("Connection", "keep-alive");
|
|
2421
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
2422
|
+
res.flushHeaders?.();
|
|
2423
|
+
res.write(`event: status
|
|
2424
|
+
data: ${JSON.stringify({
|
|
2425
|
+
running: tail.running,
|
|
2426
|
+
appId: tail.appId || null,
|
|
2427
|
+
startedAt: tail.startedAt || null,
|
|
2428
|
+
time: Date.now()
|
|
2429
|
+
})}
|
|
2430
|
+
|
|
2431
|
+
`);
|
|
2432
|
+
sseClients.add(res);
|
|
2433
|
+
const heartbeat = setInterval(() => {
|
|
2434
|
+
try {
|
|
2435
|
+
res.write(`:heartbeat
|
|
2436
|
+
|
|
2437
|
+
`);
|
|
2438
|
+
} catch {
|
|
2439
|
+
clearInterval(heartbeat);
|
|
2440
|
+
}
|
|
2441
|
+
}, 3e4);
|
|
2442
|
+
req.on("close", () => {
|
|
2443
|
+
clearInterval(heartbeat);
|
|
2444
|
+
sseClients.delete(res);
|
|
2445
|
+
});
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// server/routes/logsQuery.ts
|
|
2450
|
+
function registerLogsQueryRoutes(app2) {
|
|
2451
|
+
const logQueryArgs = (kind) => (p) => [
|
|
2452
|
+
"app",
|
|
2453
|
+
"logs",
|
|
2454
|
+
kind,
|
|
2455
|
+
"--app-id",
|
|
2456
|
+
p.clientId,
|
|
2457
|
+
...p.status ? ["--status", p.status] : [],
|
|
2458
|
+
...p.env ? ["--env", p.env] : [],
|
|
2459
|
+
...p.codes ? ["--codes", p.codes] : [],
|
|
2460
|
+
...p.traceId ? ["--trace-id", p.traceId] : [],
|
|
2461
|
+
...p.start ? ["--start", p.start] : [],
|
|
2462
|
+
...p.end ? ["--end", p.end] : [],
|
|
2463
|
+
...p.page ? ["--page", p.page] : [],
|
|
2464
|
+
...p.size ? ["--size", p.size] : []
|
|
2465
|
+
];
|
|
2466
|
+
defineCliRoute(app2, {
|
|
2467
|
+
method: "get",
|
|
2468
|
+
path: "/api/logs/api",
|
|
2469
|
+
params: {
|
|
2470
|
+
clientId: { required: true },
|
|
2471
|
+
status: {},
|
|
2472
|
+
env: {},
|
|
2473
|
+
codes: {},
|
|
2474
|
+
traceId: {},
|
|
2475
|
+
start: { numeric: true },
|
|
2476
|
+
end: { numeric: true },
|
|
2477
|
+
page: { numeric: true },
|
|
2478
|
+
size: { numeric: true }
|
|
2479
|
+
},
|
|
2480
|
+
args: logQueryArgs("api"),
|
|
2481
|
+
run: (args, p, t) => runWfDeveloperForApp(args, p.clientId, t)
|
|
2482
|
+
});
|
|
2483
|
+
defineCliRoute(app2, {
|
|
2484
|
+
method: "get",
|
|
2485
|
+
path: "/api/logs/notify",
|
|
2486
|
+
params: {
|
|
2487
|
+
clientId: { required: true },
|
|
2488
|
+
status: {},
|
|
2489
|
+
env: {},
|
|
2490
|
+
codes: {},
|
|
2491
|
+
traceId: {},
|
|
2492
|
+
start: { numeric: true },
|
|
2493
|
+
end: { numeric: true },
|
|
2494
|
+
page: { numeric: true },
|
|
2495
|
+
size: { numeric: true }
|
|
2496
|
+
},
|
|
2497
|
+
args: logQueryArgs("notify"),
|
|
2498
|
+
run: (args, p, t) => runWfDeveloperForApp(args, p.clientId, t)
|
|
2499
|
+
});
|
|
2500
|
+
defineCliRoute(app2, {
|
|
2501
|
+
method: "get",
|
|
2502
|
+
path: "/api/logs/notify/enrich",
|
|
2503
|
+
params: { clientId: { required: true }, traceId: { required: true } },
|
|
2504
|
+
args: (p) => ["app", "logs", "enrich", "--app-id", p.clientId, "--trace-id", p.traceId],
|
|
2505
|
+
run: (args, p, t) => runWfDeveloperForApp(args, p.clientId, t)
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// server/cli/localTools.ts
|
|
2510
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
2511
|
+
import { accessSync, constants, existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
2512
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join6, resolve as resolve2 } from "node:path";
|
|
2513
|
+
|
|
2514
|
+
// server/cli/executors.ts
|
|
2515
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
2516
|
+
import { existsSync as existsSync4, statSync as statSync3 } from "node:fs";
|
|
2517
|
+
function parseVersion(output) {
|
|
2518
|
+
return output.match(/(\d+\.\d+\.\d+)/)?.[1];
|
|
2519
|
+
}
|
|
2520
|
+
function parseQoderAuth(output) {
|
|
2521
|
+
const l = output.toLowerCase();
|
|
2522
|
+
if (l.includes("logged in") || l.includes("authenticated") || l.includes("\u5DF2\u767B\u5F55")) return "authenticated";
|
|
2523
|
+
if (l.includes("not logged") || l.includes("login required") || l.includes("\u672A\u767B\u5F55") || l.includes("login")) return "required";
|
|
2524
|
+
return "unknown";
|
|
2525
|
+
}
|
|
2526
|
+
function parseQoderModels(output) {
|
|
2527
|
+
return output.split("\n").map((l) => l.trim()).filter(
|
|
2528
|
+
(l) => l && !l.startsWith("#") && !l.startsWith("Available") && l.toUpperCase() !== "MODEL" && !/^-+$/.test(l) && l.length > 2
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
var EXECUTOR_SPECS = [
|
|
2532
|
+
{
|
|
2533
|
+
key: "qoder",
|
|
2534
|
+
label: "Qoder CLI",
|
|
2535
|
+
commands: ["qodercli", "qoder", "qoder-cli"],
|
|
2536
|
+
subtitle: "\u9A71\u52A8\u96C6\u6210\u6D41\u6C34\u7EBF\u7684 AI \u4F1A\u8BDD\u5F15\u64CE",
|
|
2537
|
+
qa: "\u5DF2\u63A5\u5165\uFF1A\u9A71\u52A8\u96C6\u6210\u6D41\u6C34\u7EBF\u7684 AI \u4F1A\u8BDD\u5F15\u64CE",
|
|
2538
|
+
versionArgs: ["--version"],
|
|
2539
|
+
authArgs: ["--bare", "status"],
|
|
2540
|
+
parseAuth: parseQoderAuth,
|
|
2541
|
+
modelArgs: ["--list-models"],
|
|
2542
|
+
parseModels: parseQoderModels
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
key: "codex",
|
|
2546
|
+
label: "Codex CLI",
|
|
2547
|
+
commands: ["codex"],
|
|
2548
|
+
subtitle: "OpenAI Codex \u547D\u4EE4\u884C\u6267\u884C\u8005",
|
|
2549
|
+
qa: "CLI \u901A\u9053\u95EE\u7B54\u56DE\u4F20\u534F\u8BAE\u5BF9\u63A5\u4E2D",
|
|
2550
|
+
versionArgs: ["--version"]
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
key: "claude-code",
|
|
2554
|
+
label: "Claude Code CLI",
|
|
2555
|
+
commands: ["claude", "claude-code"],
|
|
2556
|
+
subtitle: "Anthropic Claude Code \u547D\u4EE4\u884C\u6267\u884C\u8005",
|
|
2557
|
+
qa: "CLI \u901A\u9053\u95EE\u7B54\u56DE\u4F20\u534F\u8BAE\u5BF9\u63A5\u4E2D",
|
|
2558
|
+
versionArgs: ["--version"]
|
|
2559
|
+
}
|
|
2560
|
+
];
|
|
2561
|
+
var SPEC_BY_KEY = new Map(EXECUTOR_SPECS.map((s) => [s.key, s]));
|
|
2562
|
+
function isExecutorKey(key) {
|
|
2563
|
+
return SPEC_BY_KEY.has(key);
|
|
2564
|
+
}
|
|
2565
|
+
function resolveCommand(spec, manualPath) {
|
|
2566
|
+
if (manualPath && existsSync4(manualPath)) {
|
|
2567
|
+
try {
|
|
2568
|
+
if (statSync3(manualPath).isFile()) return { command: manualPath, source: "manual" };
|
|
2569
|
+
} catch {
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
const fromPath = findCommandPath(spec.commands);
|
|
2573
|
+
if (fromPath) return { command: fromPath, source: "path" };
|
|
2574
|
+
return null;
|
|
2575
|
+
}
|
|
2576
|
+
function detectExecutor(key) {
|
|
2577
|
+
const spec = SPEC_BY_KEY.get(key);
|
|
2578
|
+
const manualPath = getExecutorPaths()[key] || "";
|
|
2579
|
+
const base = {
|
|
2580
|
+
key: spec.key,
|
|
2581
|
+
label: spec.label,
|
|
2582
|
+
subtitle: spec.subtitle,
|
|
2583
|
+
qa: spec.qa,
|
|
2584
|
+
installed: false,
|
|
2585
|
+
manualPath,
|
|
2586
|
+
status: "missing",
|
|
2587
|
+
detail: "",
|
|
2588
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
2589
|
+
};
|
|
2590
|
+
const resolved = resolveCommand(spec, manualPath);
|
|
2591
|
+
if (!resolved) {
|
|
2592
|
+
if (manualPath) {
|
|
2593
|
+
base.detail = `\u624B\u52A8\u6307\u5B9A\u7684\u8DEF\u5F84\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u7528\uFF1A${manualPath}\uFF1B\u4E5F\u672A\u5728 PATH \u4E2D\u68C0\u6D4B\u5230 ${spec.commands.join(" / ")}`;
|
|
2594
|
+
} else {
|
|
2595
|
+
base.detail = `\u672A\u5728 PATH \u4E2D\u68C0\u6D4B\u5230 ${spec.commands.join(" / ")}\uFF0C\u53EF\u624B\u52A8\u6307\u5B9A\u5176\u5B89\u88C5\u8DEF\u5F84`;
|
|
2596
|
+
}
|
|
2597
|
+
return base;
|
|
2598
|
+
}
|
|
2599
|
+
base.installed = true;
|
|
2600
|
+
base.command = resolved.command;
|
|
2601
|
+
base.source = resolved.source;
|
|
2602
|
+
try {
|
|
2603
|
+
const vOut = execSync2(`"${resolved.command}" ${spec.versionArgs.join(" ")}`, { encoding: "utf-8", timeout: 5e3 });
|
|
2604
|
+
base.version = parseVersion(vOut);
|
|
2605
|
+
} catch {
|
|
2606
|
+
}
|
|
2607
|
+
if (spec.authArgs && spec.parseAuth) {
|
|
2608
|
+
try {
|
|
2609
|
+
const sOut = execSync2(`"${resolved.command}" ${spec.authArgs.join(" ")}`, { encoding: "utf-8", timeout: 5e3 });
|
|
2610
|
+
base.authentication = spec.parseAuth(sOut);
|
|
2611
|
+
} catch {
|
|
2612
|
+
base.authentication = "unknown";
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (spec.modelArgs && spec.parseModels) {
|
|
2616
|
+
try {
|
|
2617
|
+
const mOut = execSync2(`"${resolved.command}" ${spec.modelArgs.join(" ")}`, {
|
|
2618
|
+
encoding: "utf-8",
|
|
2619
|
+
timeout: 1e4,
|
|
2620
|
+
maxBuffer: 2 * 1024 * 1024
|
|
2621
|
+
});
|
|
2622
|
+
base.models = spec.parseModels(mOut);
|
|
2623
|
+
} catch {
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
const srcTip = resolved.source === "manual" ? "\uFF08\u624B\u52A8\u6307\u5B9A\uFF09" : "";
|
|
2627
|
+
if (spec.parseAuth) {
|
|
2628
|
+
if (base.authentication === "authenticated") {
|
|
2629
|
+
base.status = "available";
|
|
2630
|
+
base.detail = `\u5DF2\u68C0\u6D4B\u5230 ${spec.label}${srcTip}\uFF1A${resolved.command}\uFF1BCLI \u5DF2\u767B\u5F55`;
|
|
2631
|
+
} else if (base.authentication === "required") {
|
|
2632
|
+
base.status = "auth-required";
|
|
2633
|
+
base.detail = `\u5DF2\u68C0\u6D4B\u5230 ${spec.label}${srcTip}\uFF1A${resolved.command}\uFF1BCLI \u672A\u767B\u5F55`;
|
|
2634
|
+
} else {
|
|
2635
|
+
base.status = "unknown";
|
|
2636
|
+
base.detail = `\u5DF2\u68C0\u6D4B\u5230 ${spec.label}${srcTip}\uFF1A${resolved.command}\uFF1B\u767B\u5F55\u72B6\u6001\u672A\u77E5`;
|
|
2637
|
+
}
|
|
2638
|
+
} else {
|
|
2639
|
+
base.status = "available";
|
|
2640
|
+
base.detail = `\u5DF2\u68C0\u6D4B\u5230 ${spec.label}${srcTip}\uFF1A${resolved.command}${base.version ? `\uFF08v${base.version}\uFF09` : ""}`;
|
|
2641
|
+
}
|
|
2642
|
+
return base;
|
|
2643
|
+
}
|
|
2644
|
+
function detectAllExecutors() {
|
|
2645
|
+
return EXECUTOR_SPECS.map((s) => detectExecutor(s.key));
|
|
2646
|
+
}
|
|
2647
|
+
function resolveExecutorCommand(key) {
|
|
2648
|
+
const spec = SPEC_BY_KEY.get(key);
|
|
2649
|
+
if (!spec) return null;
|
|
2650
|
+
const manualPath = getExecutorPaths()[key] || "";
|
|
2651
|
+
return resolveCommand(spec, manualPath)?.command ?? null;
|
|
2652
|
+
}
|
|
2653
|
+
function setExecutorManualPath(key, path) {
|
|
2654
|
+
const trimmed = (path || "").trim();
|
|
2655
|
+
if (trimmed) {
|
|
2656
|
+
if (!existsSync4(trimmed)) return { ok: false, error: `\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${trimmed}` };
|
|
2657
|
+
try {
|
|
2658
|
+
if (!statSync3(trimmed).isFile()) return { ok: false, error: `\u4E0D\u662F\u53EF\u6267\u884C\u6587\u4EF6\uFF1A${trimmed}` };
|
|
2659
|
+
} catch (e) {
|
|
2660
|
+
return { ok: false, error: `\u65E0\u6CD5\u8BBF\u95EE\u8BE5\u8DEF\u5F84\uFF1A${String(e)}` };
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
try {
|
|
2664
|
+
setExecutorPath(key, trimmed);
|
|
2665
|
+
} catch (e) {
|
|
2666
|
+
return { ok: false, error: `\u8DEF\u5F84\u6301\u4E45\u5316\u5931\u8D25\uFF1A${String(e)}` };
|
|
2667
|
+
}
|
|
2668
|
+
return { ok: true, executor: detectExecutor(key) };
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// server/cli/localTools.ts
|
|
2672
|
+
function isExecutableFile(p) {
|
|
2673
|
+
try {
|
|
2674
|
+
return statSync4(p).isFile() && (accessSync(p, constants.X_OK), true);
|
|
2675
|
+
} catch {
|
|
2676
|
+
return false;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
var SEARCH_SKIPS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
2680
|
+
function parseVersionJson(out) {
|
|
2681
|
+
const text = (out || "").trim();
|
|
2682
|
+
if (!text) return { ok: false };
|
|
2683
|
+
const start = text.indexOf("{");
|
|
2684
|
+
if (start < 0) return { ok: false };
|
|
2685
|
+
try {
|
|
2686
|
+
const parsed = JSON.parse(text.slice(start));
|
|
2687
|
+
if (parsed && parsed.ok === true) {
|
|
2688
|
+
return { ok: true, version: typeof parsed.data?.version === "string" ? parsed.data.version : void 0 };
|
|
2689
|
+
}
|
|
2690
|
+
} catch {
|
|
2691
|
+
}
|
|
2692
|
+
return { ok: false };
|
|
2693
|
+
}
|
|
2694
|
+
function searchWfDeveloperUpward(startDir) {
|
|
2695
|
+
let level = resolve2(startDir);
|
|
2696
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
2697
|
+
for (const candidate of [join6(level, "wf-developer"), join6(level, "bin", "wf-developer")]) {
|
|
2698
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
2699
|
+
}
|
|
2700
|
+
try {
|
|
2701
|
+
const entries = readdirSync2(level, { withFileTypes: true }).slice(0, 200);
|
|
2702
|
+
for (const ent of entries) {
|
|
2703
|
+
if (!ent.isDirectory() || ent.name.startsWith(".") || SEARCH_SKIPS.has(ent.name)) continue;
|
|
2704
|
+
for (const candidate of [join6(level, ent.name, "bin", "wf-developer"), join6(level, ent.name, "wf-developer")]) {
|
|
2705
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
const parent = dirname5(level);
|
|
2711
|
+
if (parent === level) break;
|
|
2712
|
+
level = parent;
|
|
2713
|
+
}
|
|
2714
|
+
return null;
|
|
2715
|
+
}
|
|
2716
|
+
function verifyWfDeveloper(bin) {
|
|
2717
|
+
try {
|
|
2718
|
+
const r = spawnSync3(bin, ["version", "--json"], {
|
|
2719
|
+
encoding: "utf-8",
|
|
2720
|
+
timeout: 1e4,
|
|
2721
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2722
|
+
});
|
|
2723
|
+
return parseVersionJson(r.stdout || "");
|
|
2724
|
+
} catch {
|
|
2725
|
+
return { ok: false };
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
function detectWfDeveloperTool() {
|
|
2729
|
+
const base = {
|
|
2730
|
+
key: "wf-developer",
|
|
2731
|
+
label: "wf-developer CLI",
|
|
2732
|
+
installed: false,
|
|
2733
|
+
detail: "",
|
|
2734
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
2735
|
+
};
|
|
2736
|
+
const configured = getSettings().cliPath;
|
|
2737
|
+
if (configured) {
|
|
2738
|
+
if (!existsSync5(configured)) {
|
|
2739
|
+
base.detail = `dc_settings.cli_path \u6307\u5411\u7684\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${configured}\uFF1B\u8BF7\u5728\u542F\u52A8\u68C0\u67E5\u9875\u91CD\u65B0\u6307\u5B9A`;
|
|
2740
|
+
return base;
|
|
2741
|
+
}
|
|
2742
|
+
const v2 = verifyWfDeveloper(configured);
|
|
2743
|
+
base.command = configured;
|
|
2744
|
+
base.source = "settings";
|
|
2745
|
+
if (!v2.ok) {
|
|
2746
|
+
base.detail = `\u5DF2\u914D\u7F6E\u8DEF\u5F84 ${configured}\uFF0C\u4F46 version --json \u6821\u9A8C\u672A\u901A\u8FC7\uFF08\u9700\u8F93\u51FA ok=true \u7684 JSON\uFF09`;
|
|
2747
|
+
return base;
|
|
2748
|
+
}
|
|
2749
|
+
base.installed = true;
|
|
2750
|
+
base.version = v2.version;
|
|
2751
|
+
base.detail = `\u68C0\u6D4B\u6210\u529F\uFF08dc_settings.cli_path\uFF09\uFF1A${configured}`;
|
|
2752
|
+
return base;
|
|
2753
|
+
}
|
|
2754
|
+
const found = searchWfDeveloperUpward(process.cwd());
|
|
2755
|
+
if (!found) {
|
|
2756
|
+
base.detail = "dc_settings.cli_path \u672A\u914D\u7F6E\uFF0C\u4E14\u7236\u7EA7\u76EE\u5F55\u4E2D\u672A\u641C\u7D22\u5230 wf-developer \u53EF\u6267\u884C\u6587\u4EF6\uFF1B\u8BF7\u5728\u542F\u52A8\u68C0\u67E5\u9875\u624B\u52A8\u6307\u5B9A\u5B89\u88C5\u8DEF\u5F84";
|
|
2757
|
+
return base;
|
|
2758
|
+
}
|
|
2759
|
+
const v = verifyWfDeveloper(found);
|
|
2760
|
+
base.command = found;
|
|
2761
|
+
base.source = "searched";
|
|
2762
|
+
if (!v.ok) {
|
|
2763
|
+
base.detail = `\u7236\u7EA7\u76EE\u5F55\u641C\u7D22\u547D\u4E2D ${found}\uFF0C\u4F46 version --json \u6821\u9A8C\u672A\u901A\u8FC7\uFF08\u9700\u8F93\u51FA ok=true \u7684 JSON\uFF09`;
|
|
2764
|
+
return base;
|
|
2765
|
+
}
|
|
2766
|
+
setWfDeveloperPath(found);
|
|
2767
|
+
base.installed = true;
|
|
2768
|
+
base.version = v.version;
|
|
2769
|
+
base.detail = `\u68C0\u6D4B\u6210\u529F\uFF08\u7236\u7EA7\u76EE\u5F55\u641C\u7D22\u547D\u4E2D\uFF0C\u5DF2\u5199\u56DE cli_path\uFF09\uFF1A${found}`;
|
|
2770
|
+
return base;
|
|
2771
|
+
}
|
|
2772
|
+
function readGitVersion(bin) {
|
|
2773
|
+
try {
|
|
2774
|
+
const r = spawnSync3(bin, ["--version"], { encoding: "utf-8", timeout: 5e3, stdio: ["ignore", "pipe", "pipe"] });
|
|
2775
|
+
return parseVersion(r.stdout || "");
|
|
2776
|
+
} catch {
|
|
2777
|
+
return void 0;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
function detectGitTool() {
|
|
2781
|
+
const base = {
|
|
2782
|
+
key: "git",
|
|
2783
|
+
label: "Git",
|
|
2784
|
+
installed: false,
|
|
2785
|
+
detail: "",
|
|
2786
|
+
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
2787
|
+
};
|
|
2788
|
+
const manual = getGitPath();
|
|
2789
|
+
if (manual) {
|
|
2790
|
+
if (isExecutableFile(manual)) {
|
|
2791
|
+
base.installed = true;
|
|
2792
|
+
base.command = manual;
|
|
2793
|
+
base.source = "manual";
|
|
2794
|
+
base.version = readGitVersion(manual);
|
|
2795
|
+
base.detail = `\u68C0\u6D4B\u6210\u529F\uFF08\u624B\u52A8\u6307\u5B9A\u76EE\u5F55\uFF09\uFF1A${manual}`;
|
|
2796
|
+
return base;
|
|
2797
|
+
}
|
|
2798
|
+
base.detail = `\u624B\u52A8\u6307\u5B9A\u7684 git \u5DF2\u5931\u6548\uFF1A${manual}\uFF1B\u56DE\u9000\u73AF\u5883\u53D8\u91CF PATH \u68C0\u6D4B`;
|
|
2799
|
+
}
|
|
2800
|
+
const fromPath = findCommandPath(["git"]);
|
|
2801
|
+
if (fromPath) {
|
|
2802
|
+
base.installed = true;
|
|
2803
|
+
base.command = fromPath;
|
|
2804
|
+
base.source = "path";
|
|
2805
|
+
base.version = readGitVersion(fromPath);
|
|
2806
|
+
base.detail = base.detail ? `${base.detail}\uFF1BPATH \u547D\u4E2D\uFF1A${fromPath}` : `\u68C0\u6D4B\u6210\u529F\uFF08\u73AF\u5883\u53D8\u91CF PATH\uFF09\uFF1A${fromPath}`;
|
|
2807
|
+
return base;
|
|
2808
|
+
}
|
|
2809
|
+
base.detail = base.detail ? `${base.detail}\uFF1BPATH \u4E2D\u4E5F\u672A\u68C0\u6D4B\u5230 git\uFF0C\u8BF7\u9009\u62E9 git \u6240\u5728\u76EE\u5F55` : "\u672A\u5728\u73AF\u5883\u53D8\u91CF PATH \u4E2D\u68C0\u6D4B\u5230 git\uFF0C\u8BF7\u9009\u62E9 git \u6240\u5728\u76EE\u5F55\u540E\u91CD\u65B0\u68C0\u6D4B";
|
|
2810
|
+
return base;
|
|
2811
|
+
}
|
|
2812
|
+
function detectLocalTools() {
|
|
2813
|
+
return [detectWfDeveloperTool(), detectGitTool()];
|
|
2814
|
+
}
|
|
2815
|
+
function setGitDir(dir) {
|
|
2816
|
+
const trimmed = (dir || "").trim();
|
|
2817
|
+
if (!trimmed) {
|
|
2818
|
+
setGitPath("");
|
|
2819
|
+
return { ok: true, tools: detectLocalTools() };
|
|
2820
|
+
}
|
|
2821
|
+
if (!isAbsolute2(trimmed)) return { ok: false, error: `\u8BF7\u9009\u62E9\u7EDD\u5BF9\u8DEF\u5F84\u76EE\u5F55\uFF1A${trimmed}` };
|
|
2822
|
+
const resolved = resolve2(trimmed);
|
|
2823
|
+
try {
|
|
2824
|
+
if (!existsSync5(resolved) || !statSync4(resolved).isDirectory()) {
|
|
2825
|
+
return { ok: false, error: `\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${resolved}` };
|
|
2826
|
+
}
|
|
2827
|
+
} catch (e) {
|
|
2828
|
+
return { ok: false, error: `\u65E0\u6CD5\u8BBF\u95EE\u8BE5\u76EE\u5F55\uFF1A${String(e)}` };
|
|
2829
|
+
}
|
|
2830
|
+
const bin = join6(resolved, "git");
|
|
2831
|
+
if (!isExecutableFile(bin)) return { ok: false, error: `\u8BE5\u76EE\u5F55\u4E0B\u6CA1\u6709\u53EF\u6267\u884C\u7684 git\uFF1A${bin}` };
|
|
2832
|
+
setGitPath(bin);
|
|
2833
|
+
return { ok: true, tools: detectLocalTools() };
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// server/routes/localTools.ts
|
|
2837
|
+
function registerLocalToolRoutes(app2) {
|
|
2838
|
+
app2.get("/api/local-tools", (_req, res) => {
|
|
2839
|
+
try {
|
|
2840
|
+
res.json({ success: true, data: detectLocalTools() });
|
|
2841
|
+
} catch (err) {
|
|
2842
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
2843
|
+
}
|
|
2844
|
+
});
|
|
2845
|
+
app2.post("/api/local-tools/refresh", (_req, res) => {
|
|
2846
|
+
try {
|
|
2847
|
+
res.json({ success: true, data: detectLocalTools() });
|
|
2848
|
+
} catch (err) {
|
|
2849
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
app2.post("/api/local-tools/git-dir", (req, res) => {
|
|
2853
|
+
const { dir } = req.body;
|
|
2854
|
+
const result = setGitDir(typeof dir === "string" ? dir : "");
|
|
2855
|
+
if (!result.ok) {
|
|
2856
|
+
res.status(400).json({ success: false, error: result.error });
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
res.json({ success: true, data: result.tools });
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// server/routes/release.ts
|
|
2864
|
+
function registerReleaseRoutes(app2) {
|
|
2865
|
+
app2.get("/api/release/approval-status", async (req, res) => {
|
|
2866
|
+
const clientId = String(req.query.clientId || "").trim();
|
|
2867
|
+
const processId = String(req.query.processId || "").trim();
|
|
2868
|
+
const actionType = String(req.query.actionType || "GO_LIVE").trim();
|
|
2869
|
+
if (!clientId) {
|
|
2870
|
+
res.status(400).json({ success: false, error: "clientId \u4E3A\u5FC5\u586B" });
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
const args = ["app", "approval", "detail", "--app-id", clientId, "--action-type", actionType];
|
|
2874
|
+
if (processId) {
|
|
2875
|
+
args.push("--process-id", processId);
|
|
2876
|
+
}
|
|
2877
|
+
try {
|
|
2878
|
+
const result = await runWfDeveloperForApp(args, clientId, 3e4);
|
|
2879
|
+
sendWfResult(res, result);
|
|
2880
|
+
} catch (err) {
|
|
2881
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
// server/index.ts
|
|
2887
|
+
import cors from "cors";
|
|
2888
|
+
import { spawn as spawn4, exec } from "node:child_process";
|
|
2889
|
+
import { existsSync as existsSync6, unlinkSync as unlinkSync3, mkdirSync as mkdirSync3, copyFileSync, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync as realpathSync2 } from "node:fs";
|
|
2890
|
+
import { join as join7, dirname as dirname6 } from "node:path";
|
|
2891
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2892
|
+
import { homedir } from "node:os";
|
|
2893
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2894
|
+
var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
|
|
2895
|
+
var app = express();
|
|
2896
|
+
var PORT = Number(process.env.PORT || 51689);
|
|
2897
|
+
app.use(cors());
|
|
2898
|
+
app.use(express.json({ limit: "30mb" }));
|
|
2899
|
+
function detectQoderSync() {
|
|
2900
|
+
const e = detectExecutor("qoder");
|
|
2901
|
+
return {
|
|
2902
|
+
id: "qoder",
|
|
2903
|
+
label: "Qoder",
|
|
2904
|
+
installed: e.installed,
|
|
2905
|
+
command: e.command,
|
|
2906
|
+
status: e.status,
|
|
2907
|
+
authentication: e.authentication,
|
|
2908
|
+
version: e.version,
|
|
2909
|
+
models: e.models,
|
|
2910
|
+
detail: e.detail,
|
|
2911
|
+
lastSeen: e.lastSeen
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
function checkSkills() {
|
|
2915
|
+
const skillDefs = [
|
|
2916
|
+
{ name: "wf-api-intent-recognition", label: "API \u96C6\u6210\u610F\u56FE\u8BC6\u522B" },
|
|
2917
|
+
{ name: "wf-webhook-setting", label: "Webhook \u4E8B\u4EF6\u8BA2\u9605\u914D\u7F6E" },
|
|
2918
|
+
{ name: "wf-security-setting", label: "\u5B89\u5168\u8BBE\u7F6E\uFF08RSA \u516C\u94A5\u914D\u7F6E\uFF09" },
|
|
2919
|
+
{ name: "wf-integration-case", label: "\u96C6\u6210\u7528\u4F8B\u751F\u6210" }
|
|
2920
|
+
];
|
|
2921
|
+
return skillDefs.map((def) => {
|
|
2922
|
+
const qoderPath = join7(QODER_SKILLS_DIR, def.name, "SKILL.md");
|
|
2923
|
+
const projectPath = join7(PROJECT_SKILLS_DIR, def.name, "SKILL.md");
|
|
2924
|
+
return { name: def.name, label: def.label, installed: existsSync6(qoderPath), projectPath, qoderPath };
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
function installSkill(name) {
|
|
2928
|
+
const projectSkillDir = join7(PROJECT_SKILLS_DIR, name);
|
|
2929
|
+
const qoderSkillDir = join7(QODER_SKILLS_DIR, name);
|
|
2930
|
+
if (!existsSync6(join7(projectSkillDir, "SKILL.md"))) return { success: false, message: `\u5DE5\u7A0B\u5185\u672A\u627E\u5230 Skill: ${name}` };
|
|
2931
|
+
try {
|
|
2932
|
+
copySkillDir(projectSkillDir, qoderSkillDir);
|
|
2933
|
+
return { success: true, message: `Skill ${name} \u5B89\u88C5\u6210\u529F` };
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
return { success: false, message: `\u5B89\u88C5\u5931\u8D25: ${String(err)}` };
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
function copySkillDir(srcDir, destDir) {
|
|
2939
|
+
mkdirSync3(destDir, { recursive: true });
|
|
2940
|
+
for (const entry of readdirSync3(srcDir, { withFileTypes: true })) {
|
|
2941
|
+
const src = join7(srcDir, entry.name);
|
|
2942
|
+
const dest = join7(destDir, entry.name);
|
|
2943
|
+
if (entry.isDirectory()) copySkillDir(src, dest);
|
|
2944
|
+
else if (entry.isFile()) copyFileSync(src, dest);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
function skillDirInSync(srcDir, destDir) {
|
|
2948
|
+
if (!existsSync6(destDir)) return false;
|
|
2949
|
+
for (const entry of readdirSync3(srcDir, { withFileTypes: true })) {
|
|
2950
|
+
const src = join7(srcDir, entry.name);
|
|
2951
|
+
const dest = join7(destDir, entry.name);
|
|
2952
|
+
if (entry.isDirectory()) {
|
|
2953
|
+
if (!skillDirInSync(src, dest)) return false;
|
|
2954
|
+
} else if (entry.isFile()) {
|
|
2955
|
+
if (!existsSync6(dest) || !readFileSync4(src).equals(readFileSync4(dest))) return false;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
return true;
|
|
2959
|
+
}
|
|
2960
|
+
function syncSkillsToQoder() {
|
|
2961
|
+
const results = [];
|
|
2962
|
+
if (!existsSync6(PROJECT_SKILLS_DIR)) return results;
|
|
2963
|
+
for (const name of readdirSync3(PROJECT_SKILLS_DIR)) {
|
|
2964
|
+
const srcDir = join7(PROJECT_SKILLS_DIR, name);
|
|
2965
|
+
if (!existsSync6(join7(srcDir, "SKILL.md"))) continue;
|
|
2966
|
+
const destDir = join7(QODER_SKILLS_DIR, name);
|
|
2967
|
+
if (skillDirInSync(srcDir, destDir)) continue;
|
|
2968
|
+
try {
|
|
2969
|
+
copySkillDir(srcDir, destDir);
|
|
2970
|
+
results.push({ name, success: true, message: `Skill ${name} \u5DF2\u540C\u6B65\u5B89\u88C5` });
|
|
2971
|
+
console.log(`[developer-console] skill auto-installed to Qoder: ${name}`);
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
results.push({ name, success: false, message: `\u5B89\u88C5\u5931\u8D25: ${String(err)}` });
|
|
2974
|
+
console.warn(`[developer-console] skill auto-install failed: ${name}: ${String(err)}`);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
return results;
|
|
2978
|
+
}
|
|
2979
|
+
function checkMcps() {
|
|
2980
|
+
return [];
|
|
2981
|
+
}
|
|
2982
|
+
var APPS_CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
2983
|
+
var appsCatalogCache = null;
|
|
2984
|
+
async function fetchAppsCatalog(force = false) {
|
|
2985
|
+
if (!force && appsCatalogCache && Date.now() - appsCatalogCache.at < APPS_CATALOG_TTL_MS) {
|
|
2986
|
+
return { ok: true, data: appsCatalogCache.data };
|
|
2987
|
+
}
|
|
2988
|
+
const r = await runWfDeveloper(["app", "catalog"], 9e4);
|
|
2989
|
+
if (r.ok) appsCatalogCache = { at: Date.now(), data: r.data };
|
|
2990
|
+
return r;
|
|
2991
|
+
}
|
|
2992
|
+
var APP_NAME_PATTERN = /^[A-Za-z0-9_\u4e00-\u9fa5 ]+$/;
|
|
2993
|
+
function countBoundInterfaces(catalog, requestedIds) {
|
|
2994
|
+
const requested = new Set(requestedIds.map(Number));
|
|
2995
|
+
let bound = 0;
|
|
2996
|
+
const walkModules = (mods) => {
|
|
2997
|
+
for (const mod of mods || []) {
|
|
2998
|
+
for (const itf of mod?.interfaceList || []) {
|
|
2999
|
+
if (itf?.integrated && requested.has(Number(itf.interfaceId))) bound++;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
for (const cat of catalog?.categoryList || []) walkModules(cat?.moduleList || []);
|
|
3004
|
+
walkModules(catalog?.moduleList || []);
|
|
3005
|
+
return bound;
|
|
3006
|
+
}
|
|
3007
|
+
app.get("/api/apps/catalog", async (req, res) => {
|
|
3008
|
+
sendWfResult(res, await fetchAppsCatalog(req.query.refresh === "1"));
|
|
3009
|
+
});
|
|
3010
|
+
app.get("/api/apps/list", async (_req, res) => {
|
|
3011
|
+
sendWfResult(res, await runWfDeveloper(["app", "list"], 9e4));
|
|
3012
|
+
});
|
|
3013
|
+
app.get("/api/apps/overview", async (req, res) => {
|
|
3014
|
+
const clientId = String(req.query.clientId || "").trim();
|
|
3015
|
+
if (!clientId) {
|
|
3016
|
+
res.status(400).json({ success: false, error: "clientId \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
sendWfResult(res, await runAppScopedCli(["app", "overview", "--app-id", clientId], clientId, 9e4));
|
|
3020
|
+
});
|
|
3021
|
+
app.post("/api/apps/create", async (req, res) => {
|
|
3022
|
+
const { name, interfaceIds } = req.body;
|
|
3023
|
+
const appName = (name || "").trim();
|
|
3024
|
+
if (!appName || appName.length < 3 || appName.length > 20 || !APP_NAME_PATTERN.test(appName)) {
|
|
3025
|
+
res.status(400).json({ success: false, error: "\u5E94\u7528\u540D\u4EC5\u652F\u6301\u4E2D\u6587\u3001\u82F1\u6587\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u7A7A\u683C\uFF0C\u957F\u5EA6 3-20 \u5B57\u7B26" });
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const ids = Array.isArray(interfaceIds) ? [...new Set(interfaceIds.map((v) => Number(v)).filter((n) => Number.isInteger(n) && n > 0))] : [];
|
|
3029
|
+
if (ids.length === 0) {
|
|
3030
|
+
res.status(400).json({ success: false, error: "interfaceIds \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u63A5\u53E3 ID\uFF08\u6765\u81EA apps catalog\uFF09" });
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
const r = await runWfDeveloper(["app", "create", "--name", appName, "--interface-ids", ids.join(","), "--consent"], 12e4);
|
|
3034
|
+
if (!r.ok) {
|
|
3035
|
+
sendWfResult(res, r);
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
const createdAppId = String(r.data?.appId || "");
|
|
3039
|
+
if (createdAppId) {
|
|
3040
|
+
const verify = await runAppScopedCli(["app", "catalog", "--app-id", createdAppId], createdAppId, 9e4);
|
|
3041
|
+
if (verify.ok) {
|
|
3042
|
+
const bound = countBoundInterfaces(verify.data, ids);
|
|
3043
|
+
if (bound < ids.length) {
|
|
3044
|
+
sendWfResult(res, {
|
|
3045
|
+
ok: false,
|
|
3046
|
+
cmd: r.cmd,
|
|
3047
|
+
traceId: r.traceId,
|
|
3048
|
+
error: `\u5E94\u7528\u5DF2\u521B\u5EFA\uFF08appId: ${createdAppId}\uFF09\uFF0C\u4F46\u63A5\u53E3\u7ED1\u5B9A\u4E0D\u5B8C\u6574\uFF1A\u8BF7\u6C42 ${ids.length} \u4E2A\uFF0C\u5B9E\u9645\u7ED1\u5B9A ${bound} \u4E2A\u3002\u63A5\u53E3 ID \u53EF\u80FD\u5DF2\u5931\u6548\uFF08\u5982\u73AF\u5883\u5207\u6362\u6216\u76EE\u5F55\u66F4\u65B0\uFF09\uFF0C\u8BF7\u56DE\u5230\u300C\u786E\u8BA4\u65B9\u6848\u300D\u91CD\u65B0\u9009\u62E9\u63A5\u53E3\u540E\u91CD\u8BD5\uFF1B\u5DF2\u521B\u5EFA\u7684\u5E94\u7528\u53EF\u7528 wf-developer apps update-interfaces --app-id ${createdAppId} \u8865\u9F50\u63A5\u53E3\u3002`
|
|
3049
|
+
});
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
sendWfResult(res, r);
|
|
3055
|
+
});
|
|
3056
|
+
async function runAppScopedCli(args, appId, timeoutMs = 9e4) {
|
|
3057
|
+
const r = await runWfDeveloperForApp(args, appId, timeoutMs);
|
|
3058
|
+
if (!r.ok && !appId.startsWith("SANDBOX_") && /app not publish/i.test(r.error || "")) {
|
|
3059
|
+
const sandboxId = `SANDBOX_${appId}`;
|
|
3060
|
+
const retryArgs = args.map((a) => a === appId ? sandboxId : a);
|
|
3061
|
+
return runWfDeveloperForApp(retryArgs, sandboxId, timeoutMs);
|
|
3062
|
+
}
|
|
3063
|
+
return r;
|
|
3064
|
+
}
|
|
3065
|
+
var WEBHOOK_EVENTS_TTL_MS = 5 * 60 * 1e3;
|
|
3066
|
+
var webhookEventsCache = /* @__PURE__ */ new Map();
|
|
3067
|
+
async function fetchWebhookEvents(clientId, force = false) {
|
|
3068
|
+
const cached = webhookEventsCache.get(clientId);
|
|
3069
|
+
if (!force && cached && Date.now() - cached.at < WEBHOOK_EVENTS_TTL_MS) {
|
|
3070
|
+
return { ok: true, data: cached.data };
|
|
3071
|
+
}
|
|
3072
|
+
const r = await runAppScopedCli(["app", "webhooks", "events", "--app-id", clientId], clientId, 9e4);
|
|
3073
|
+
if (r.ok) webhookEventsCache.set(clientId, { at: Date.now(), data: r.data });
|
|
3074
|
+
return r;
|
|
3075
|
+
}
|
|
3076
|
+
app.get("/api/webhooks/events", async (req, res) => {
|
|
3077
|
+
const clientId = String(req.query.clientId || "").trim();
|
|
3078
|
+
if (!clientId) {
|
|
3079
|
+
res.status(400).json({ success: false, error: "clientId \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
const configId = Number(req.query.configId || 0);
|
|
3083
|
+
if (Number.isInteger(configId) && configId > 0) {
|
|
3084
|
+
sendWfResult(res, await runAppScopedCli(["app", "webhooks", "events", "--app-id", clientId, "--config-id", String(configId)], clientId, 9e4));
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
sendWfResult(res, await fetchWebhookEvents(clientId, req.query.refresh === "1"));
|
|
3088
|
+
});
|
|
3089
|
+
app.get("/api/webhooks/list", async (req, res) => {
|
|
3090
|
+
const clientId = String(req.query.clientId || "").trim();
|
|
3091
|
+
if (!clientId) {
|
|
3092
|
+
res.status(400).json({ success: false, error: "clientId \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
sendWfResult(res, await runAppScopedCli(["app", "webhooks", "list", "--app-id", clientId], clientId, 9e4));
|
|
3096
|
+
});
|
|
3097
|
+
var MAX_WEBHOOK_CONFIGS_PER_APP = 5;
|
|
3098
|
+
var MAX_WEBHOOK_EVENTS_PER_CONFIG = 100;
|
|
3099
|
+
app.post("/api/webhooks/subscribe", async (req, res) => {
|
|
3100
|
+
const { clientId: rawClientId, name: rawName, events, webhookIds, url: rawUrl, configId: rawConfigId } = req.body;
|
|
3101
|
+
const clientId = (rawClientId || "").trim();
|
|
3102
|
+
if (!clientId) {
|
|
3103
|
+
res.status(400).json({ success: false, error: "clientId \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
const configId = Number(rawConfigId || 0);
|
|
3107
|
+
const updating = Number.isInteger(configId) && configId > 0;
|
|
3108
|
+
const hookName = (rawName || "").trim();
|
|
3109
|
+
if (hookName && (hookName.length > 30 || !APP_NAME_PATTERN.test(hookName))) {
|
|
3110
|
+
res.status(400).json({ success: false, error: "Webhook \u540D\u79F0\u4EC5\u652F\u6301\u4E2D\u6587\u3001\u82F1\u6587\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u7A7A\u683C\uFF0C\u957F\u5EA6 1-30 \u5B57\u7B26\uFF08\u540C\u5E94\u7528\u5185\u552F\u4E00\uFF09" });
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
if (!updating && !hookName) {
|
|
3114
|
+
res.status(400).json({ success: false, error: "Webhook \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\uFF08\u521B\u5EFA\u65F6\u5FC5\u586B\uFF0C\u540C\u5E94\u7528\u5185\u552F\u4E00\uFF09" });
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
const hookUrl = (rawUrl || "").trim();
|
|
3118
|
+
if (hookUrl && !/^https?:\/\/.+/.test(hookUrl)) {
|
|
3119
|
+
res.status(400).json({ success: false, error: "url \u5E94\u4E3A\u5408\u6CD5\u7684 http/https \u5730\u5740" });
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (!updating && !hookUrl) {
|
|
3123
|
+
res.status(400).json({ success: false, error: "url \u4E0D\u80FD\u4E3A\u7A7A\uFF08\u521B\u5EFA\u65F6\u5FC5\u586B\uFF09" });
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
let ids = Array.isArray(webhookIds) ? [...new Set(webhookIds.map((v) => Number(v)).filter((n) => Number.isInteger(n) && n > 0))] : [];
|
|
3127
|
+
if (ids.length === 0 && Array.isArray(events) && events.length > 0) {
|
|
3128
|
+
const catalog = await fetchWebhookEvents(clientId);
|
|
3129
|
+
if (!catalog.ok) {
|
|
3130
|
+
sendWfResult(res, catalog);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
const nameToId = /* @__PURE__ */ new Map();
|
|
3134
|
+
for (const g of catalog.data || []) {
|
|
3135
|
+
for (const it of g?.webhookList || []) {
|
|
3136
|
+
const key = String(it?.funcName || it?.url || "");
|
|
3137
|
+
const id = Number(it?.interfaceId);
|
|
3138
|
+
if (key && Number.isInteger(id) && id > 0) nameToId.set(key, id);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
const names = events.map(String).map((s) => s.trim()).filter(Boolean);
|
|
3142
|
+
const unknown = names.filter((n) => !nameToId.has(n));
|
|
3143
|
+
if (unknown.length > 0) {
|
|
3144
|
+
res.status(400).json({ success: false, error: `\u4E8B\u4EF6\u4E0D\u5728\u53EF\u8BA2\u9605\u76EE\u5F55\u4E2D: ${unknown.join(", ")}` });
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
ids = [...new Set(names.map((n) => nameToId.get(n)))];
|
|
3148
|
+
}
|
|
3149
|
+
if (!updating && ids.length === 0) {
|
|
3150
|
+
res.status(400).json({ success: false, error: "events/webhookIds \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u4E8B\u4EF6\uFF08\u89C1 webhooks events \u76EE\u5F55\uFF09" });
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
if (ids.length > MAX_WEBHOOK_EVENTS_PER_CONFIG) {
|
|
3154
|
+
res.status(400).json({ success: false, error: `\u6BCF\u4E2A Webhook \u914D\u7F6E\u6700\u591A\u8BA2\u9605 ${MAX_WEBHOOK_EVENTS_PER_CONFIG} \u4E2A\u4E8B\u4EF6\uFF0C\u672C\u6B21\u4F20\u5165\u4E86 ${ids.length} \u4E2A` });
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
if (!updating) {
|
|
3158
|
+
const list = await runAppScopedCli(["app", "webhooks", "list", "--app-id", clientId], clientId, 9e4);
|
|
3159
|
+
if (list.ok && Number(list.data?.total || 0) >= MAX_WEBHOOK_CONFIGS_PER_APP) {
|
|
3160
|
+
res.status(400).json({ success: false, error: `\u4E00\u4E2A\u5E94\u7528\u6700\u591A\u521B\u5EFA ${MAX_WEBHOOK_CONFIGS_PER_APP} \u4E2A Webhook \u8BA2\u9605\u914D\u7F6E\uFF0C\u5F53\u524D\u5DF2\u8FBE\u4E0A\u9650\u3002\u8BF7\u5220\u9664\u6216\u4FEE\u6539\u5DF2\u6709\u914D\u7F6E\u3002` });
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
const cliArgs = ["app", "webhooks", "subscribe", "--app-id", clientId];
|
|
3165
|
+
if (updating) cliArgs.push("--config-id", String(configId));
|
|
3166
|
+
if (hookName) cliArgs.push("--name", hookName);
|
|
3167
|
+
if (ids.length > 0) cliArgs.push("--webhook-id", ids.join(","));
|
|
3168
|
+
if (hookUrl) cliArgs.push("--url", hookUrl);
|
|
3169
|
+
const r = await runAppScopedCli(cliArgs, clientId, 12e4);
|
|
3170
|
+
webhookEventsCache.delete(clientId);
|
|
3171
|
+
if (!r.ok) {
|
|
3172
|
+
sendWfResult(res, r);
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
const cfg = r.data?.config || {};
|
|
3176
|
+
res.json({ success: true, data: {
|
|
3177
|
+
subscribed: true,
|
|
3178
|
+
updated: updating,
|
|
3179
|
+
configId: cfg.id || (updating ? configId : void 0),
|
|
3180
|
+
name: cfg.name || hookName,
|
|
3181
|
+
webhookUrl: cfg.url || hookUrl,
|
|
3182
|
+
// subscribe 响应的 eventCount 可能未填充(为 0),回退为本次请求的事件数
|
|
3183
|
+
count: cfg.eventCount || ids.length,
|
|
3184
|
+
config: cfg
|
|
3185
|
+
} });
|
|
3186
|
+
});
|
|
3187
|
+
app.post("/api/webhooks/delete", async (req, res) => {
|
|
3188
|
+
const { clientId: rawClientId, id } = req.body;
|
|
3189
|
+
const recordId = Number(id);
|
|
3190
|
+
if (!Number.isInteger(recordId) || recordId <= 0) {
|
|
3191
|
+
res.status(400).json({ success: false, error: "id\uFF08Webhook \u914D\u7F6E ID\uFF0C\u6B63\u6574\u6570\uFF09\u4E0D\u80FD\u4E3A\u7A7A" });
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
const clientId = (rawClientId || "").trim();
|
|
3195
|
+
const r = clientId ? await runWfDeveloperForApp(["app", "webhooks", "delete", "--id", String(recordId), "--yes"], clientId, 9e4) : await runWfDeveloper(["app", "webhooks", "delete", "--id", String(recordId), "--yes"], 9e4);
|
|
3196
|
+
if (r.ok && clientId) webhookEventsCache.delete(clientId);
|
|
3197
|
+
sendWfResult(res, r);
|
|
3198
|
+
});
|
|
3199
|
+
var PUBLIC_KEY_PATTERN = /-----BEGIN PUBLIC KEY-----[\s\S]+-----END PUBLIC KEY-----/;
|
|
3200
|
+
app.post("/api/security/set-key", async (req, res) => {
|
|
3201
|
+
const { clientId: rawClientId, publicKey: rawKey } = req.body;
|
|
3202
|
+
const clientId = (rawClientId || "").trim();
|
|
3203
|
+
if (!clientId) {
|
|
3204
|
+
res.status(400).json({ success: false, error: "clientId \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
const publicKey = (rawKey || "").trim();
|
|
3208
|
+
if (!PUBLIC_KEY_PATTERN.test(publicKey)) {
|
|
3209
|
+
res.status(400).json({ success: false, error: "\u516C\u94A5\u5E94\u4E3A PEM \u683C\u5F0F\uFF08-----BEGIN PUBLIC KEY----- \u5305\u88F9\uFF09" });
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
if (/PRIVATE KEY/.test(publicKey)) {
|
|
3213
|
+
res.status(400).json({ success: false, error: "\u68C0\u6D4B\u5230\u79C1\u94A5\u5185\u5BB9\uFF0C\u8BF7\u4EC5\u63D0\u4EA4\u516C\u94A5" });
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
const r = await runAppScopedCli(
|
|
3217
|
+
["app", "set-key", "--app-id", clientId, "--key", publicKey],
|
|
3218
|
+
clientId,
|
|
3219
|
+
12e4
|
|
3220
|
+
);
|
|
3221
|
+
sendWfResult(res, r);
|
|
3222
|
+
});
|
|
3223
|
+
app.get("/api/security/platform-key", async (req, res) => {
|
|
3224
|
+
const clientId = String(req.query.clientId || "").trim();
|
|
3225
|
+
if (!clientId) {
|
|
3226
|
+
res.status(400).json({ success: false, error: "clientId \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
const r = await runAppScopedCli(["app", "platform-key", "--app-id", clientId], clientId, 9e4);
|
|
3230
|
+
sendWfResult(res, r);
|
|
3231
|
+
});
|
|
3232
|
+
registerFileRoutes(app);
|
|
3233
|
+
app.get("/api/health", async (_req, res) => {
|
|
3234
|
+
try {
|
|
3235
|
+
const runtime = detectQoderSync();
|
|
3236
|
+
const skills = checkSkills();
|
|
3237
|
+
const mcps = checkMcps();
|
|
3238
|
+
const wfDeveloper = detectWfDeveloperSync();
|
|
3239
|
+
res.json({ server: { status: "ok", host: "127.0.0.1", port: PORT }, runtime, skills, mcps, wfDeveloper });
|
|
3240
|
+
} catch (err) {
|
|
3241
|
+
res.status(500).json({ server: { status: "error", host: "127.0.0.1", port: PORT }, error: String(err), runtime: { id: "qoder", label: "Qoder", installed: false, status: "unknown", detail: `\u68C0\u6D4B\u5931\u8D25: ${String(err)}` }, skills: [], mcps: [], wfDeveloper: { installed: false, loggedIn: false, detail: "\u68C0\u6D4B\u5931\u8D25" } });
|
|
3242
|
+
}
|
|
3243
|
+
});
|
|
3244
|
+
app.post("/api/runtime/qoder/refresh", async (_req, res) => {
|
|
3245
|
+
try {
|
|
3246
|
+
res.json({ runtime: detectQoderSync() });
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
res.status(500).json({ error: String(err) });
|
|
3249
|
+
}
|
|
3250
|
+
});
|
|
3251
|
+
app.get("/api/executors", (_req, res) => {
|
|
3252
|
+
try {
|
|
3253
|
+
res.json({ success: true, data: detectAllExecutors() });
|
|
3254
|
+
} catch (err) {
|
|
3255
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
app.post("/api/executors/refresh", (_req, res) => {
|
|
3259
|
+
try {
|
|
3260
|
+
res.json({ success: true, data: detectAllExecutors() });
|
|
3261
|
+
} catch (err) {
|
|
3262
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
app.post("/api/executors/set-path", (req, res) => {
|
|
3266
|
+
const { key, path } = req.body;
|
|
3267
|
+
if (!key || !isExecutorKey(key)) {
|
|
3268
|
+
res.status(400).json({ success: false, error: "key \u5FC5\u987B\u4E3A qoder / codex / claude-code" });
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
const result = setExecutorManualPath(key, typeof path === "string" ? path : "");
|
|
3272
|
+
if (!result.ok) {
|
|
3273
|
+
res.status(400).json({ success: false, error: result.error });
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
res.json({ success: true, data: result.executor });
|
|
3277
|
+
});
|
|
3278
|
+
app.post("/api/wf-developer/base-url", (req, res) => {
|
|
3279
|
+
const { baseUrl } = req.body;
|
|
3280
|
+
if (baseUrl && !/^https?:\/\//.test(baseUrl)) {
|
|
3281
|
+
res.status(400).json({ success: false, error: "base_url \u5FC5\u987B\u4EE5 http:// \u6216 https:// \u5F00\u5934" });
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
res.json({ success: true, baseUrl: setBaseUrl(baseUrl || "") });
|
|
3285
|
+
});
|
|
3286
|
+
app.get("/api/wf-developer/base-url", (_req, res) => {
|
|
3287
|
+
res.json({ baseUrl: getBaseUrl() });
|
|
3288
|
+
});
|
|
3289
|
+
app.get("/api/wf-developer/status", (_req, res) => {
|
|
3290
|
+
try {
|
|
3291
|
+
res.json({ wfDeveloper: detectWfDeveloperSync() });
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
res.status(500).json({ error: String(err) });
|
|
3294
|
+
}
|
|
3295
|
+
});
|
|
3296
|
+
app.get("/api/cli/set-path", (_req, res) => {
|
|
3297
|
+
res.json({ success: true, path: getWfDeveloperPath() });
|
|
3298
|
+
});
|
|
3299
|
+
app.post("/api/cli/set-path", (req, res) => {
|
|
3300
|
+
const { path: rawPath } = req.body;
|
|
3301
|
+
const result = setWfDeveloperPath(rawPath || "");
|
|
3302
|
+
if (!result.ok) {
|
|
3303
|
+
res.status(400).json({ success: false, error: result.error });
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
res.json({ success: true, path: result.path, wfDeveloper: detectWfDeveloperSync() });
|
|
3307
|
+
});
|
|
3308
|
+
var wfLoginSession = null;
|
|
3309
|
+
function cleanupWfLoginSession() {
|
|
3310
|
+
if (wfLoginSession) {
|
|
3311
|
+
try {
|
|
3312
|
+
wfLoginSession.child.kill("SIGKILL");
|
|
3313
|
+
} catch {
|
|
3314
|
+
}
|
|
3315
|
+
wfLoginSession = null;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
app.post("/api/wf-developer/login", async (_req, res) => {
|
|
3319
|
+
const bin = findWfDeveloper();
|
|
3320
|
+
if (!bin) {
|
|
3321
|
+
res.status(503).json({ success: false, error: "\u672A\u627E\u5230 wf-developer CLI" });
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
if (wfLoginSession && !wfLoginSession.finished && wfLoginSession.userCode) {
|
|
3325
|
+
res.json({ success: true, userCode: wfLoginSession.userCode, verificationUrl: wfLoginSession.verificationUrl });
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
cleanupWfLoginSession();
|
|
3329
|
+
const child = spawn4(bin, [...baseUrlArgs(), "login", "--no-browser"], { env: { ...process.env }, stdio: ["pipe", "pipe", "pipe"] });
|
|
3330
|
+
const session = { child, startedAt: Date.now(), finished: false, stderrTail: "" };
|
|
3331
|
+
wfLoginSession = session;
|
|
3332
|
+
let stderrBuf = "";
|
|
3333
|
+
const timer = setTimeout(() => {
|
|
3334
|
+
try {
|
|
3335
|
+
child.kill("SIGKILL");
|
|
3336
|
+
} catch {
|
|
3337
|
+
}
|
|
3338
|
+
}, 3e5);
|
|
3339
|
+
timer.unref?.();
|
|
3340
|
+
child.stdout.on("data", () => {
|
|
3341
|
+
});
|
|
3342
|
+
child.stderr.on("data", (c) => {
|
|
3343
|
+
stderrBuf += c.toString();
|
|
3344
|
+
session.stderrTail = stderrBuf.slice(-500);
|
|
3345
|
+
if (!session.verificationUrl) {
|
|
3346
|
+
const m = stderrBuf.match(/(?:地\s*址|URL)\s*[::]\s*(\S+)/i);
|
|
3347
|
+
if (m) session.verificationUrl = m[1];
|
|
3348
|
+
}
|
|
3349
|
+
if (!session.userCode) {
|
|
3350
|
+
const m = stderrBuf.match(/(?:验证码|Code)\s*[::]\s*([A-Za-z0-9-]+)/i);
|
|
3351
|
+
if (m) session.userCode = m[1];
|
|
3352
|
+
}
|
|
3353
|
+
if (!session.browserUrl && session.userCode && session.verificationUrl) {
|
|
3354
|
+
session.browserUrl = `${session.verificationUrl}?user_code=${encodeURIComponent(session.userCode)}`;
|
|
3355
|
+
}
|
|
3356
|
+
});
|
|
3357
|
+
child.on("error", () => {
|
|
3358
|
+
session.finished = true;
|
|
3359
|
+
});
|
|
3360
|
+
child.on("close", (code) => {
|
|
3361
|
+
clearTimeout(timer);
|
|
3362
|
+
session.finished = true;
|
|
3363
|
+
session.exitCode = code;
|
|
3364
|
+
});
|
|
3365
|
+
const deadline = Date.now() + 2e4;
|
|
3366
|
+
while (!session.userCode && !session.finished && Date.now() < deadline) {
|
|
3367
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
3368
|
+
}
|
|
3369
|
+
if (session.userCode) {
|
|
3370
|
+
res.json({ success: true, userCode: session.userCode, verificationUrl: session.verificationUrl });
|
|
3371
|
+
} else {
|
|
3372
|
+
const error = session.finished ? session.exitCode === 0 ? "\u5F53\u524D\u5DF2\u662F\u6388\u6743\u72B6\u6001\uFF0C\u65E0\u9700\u91CD\u590D\u6388\u6743" : `\u6388\u6743\u8FDB\u7A0B\u5F02\u5E38\u9000\u51FA (exit ${session.exitCode})\uFF0C${session.stderrTail.trim()}` : "\u83B7\u53D6\u6388\u6743\u7801\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u70B9\u51FB\u6388\u6743";
|
|
3373
|
+
cleanupWfLoginSession();
|
|
3374
|
+
res.json({ success: false, error });
|
|
3375
|
+
}
|
|
3376
|
+
});
|
|
3377
|
+
app.post("/api/wf-developer/login/open-browser", (_req, res) => {
|
|
3378
|
+
const session = wfLoginSession && !wfLoginSession.finished ? wfLoginSession : void 0;
|
|
3379
|
+
const url = session?.browserUrl || session?.verificationUrl;
|
|
3380
|
+
if (!url) {
|
|
3381
|
+
res.status(404).json({ success: false, error: "\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684\u6388\u6743\u4F1A\u8BDD\uFF0C\u8BF7\u5148\u70B9\u51FB\u6388\u6743" });
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
3385
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
3386
|
+
const child = spawn4(opener, args, { stdio: "ignore" });
|
|
3387
|
+
child.on("error", (err) => res.status(500).json({ success: false, error: `\u6253\u5F00\u6D4F\u89C8\u5668\u5931\u8D25: ${String(err)}` }));
|
|
3388
|
+
child.on("close", (code) => {
|
|
3389
|
+
if (code === 0) res.json({ success: true });
|
|
3390
|
+
else res.status(500).json({ success: false, error: `\u6253\u5F00\u6D4F\u89C8\u5668\u5931\u8D25 (exit ${code})` });
|
|
3391
|
+
});
|
|
3392
|
+
});
|
|
3393
|
+
app.post("/api/wf-developer/login/cancel", (_req, res) => {
|
|
3394
|
+
cleanupWfLoginSession();
|
|
3395
|
+
res.json({ success: true });
|
|
3396
|
+
});
|
|
3397
|
+
app.get("/api/skills/check", (_req, res) => {
|
|
3398
|
+
res.json({ skills: checkSkills() });
|
|
3399
|
+
});
|
|
3400
|
+
app.post("/api/skills/install", (req, res) => {
|
|
3401
|
+
const { name } = req.body;
|
|
3402
|
+
if (!name) {
|
|
3403
|
+
res.status(400).json({ error: "name is required" });
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
res.json(installSkill(name));
|
|
3407
|
+
});
|
|
3408
|
+
app.post("/api/skills/install-all", (_req, res) => {
|
|
3409
|
+
const results = syncSkillsToQoder();
|
|
3410
|
+
res.json({ results, allInstalled: checkSkills().every((s) => s.installed) });
|
|
3411
|
+
});
|
|
3412
|
+
app.post("/api/system/browse", (req, res) => {
|
|
3413
|
+
const { kind } = req.body;
|
|
3414
|
+
const isDir = kind !== "file";
|
|
3415
|
+
const prompt = isDir ? "\u9009\u62E9\u9879\u76EE\u6839\u76EE\u5F55" : "\u9009\u62E9\u6587\u4EF6";
|
|
3416
|
+
if (process.platform !== "darwin") {
|
|
3417
|
+
res.status(501).json({ success: false, error: "\u5F53\u524D\u7CFB\u7EDF\u6682\u4E0D\u652F\u6301\u539F\u751F\u9009\u62E9\u7A97\u53E3\uFF0C\u8BF7\u624B\u52A8\u8F93\u5165\u8DEF\u5F84" });
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
const script = isDir ? `POSIX path of (choose folder with prompt "${prompt}")` : `POSIX path of (choose file with prompt "${prompt}")`;
|
|
3421
|
+
const cmd = `osascript -e 'tell application "System Events" to activate' -e '${script}'`;
|
|
3422
|
+
exec(cmd, { timeout: 3e5 }, (err, stdout) => {
|
|
3423
|
+
if (err) {
|
|
3424
|
+
if (String(err).includes("User canceled") || String(err.stderr || "").includes("User canceled")) {
|
|
3425
|
+
res.json({ success: false, cancelled: true });
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
res.status(500).json({ success: false, error: String(err) });
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
let p = stdout.trim();
|
|
3432
|
+
if (isDir && p.endsWith("/")) p = p.slice(0, -1);
|
|
3433
|
+
res.json({ success: true, path: p });
|
|
3434
|
+
});
|
|
3435
|
+
});
|
|
3436
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
3437
|
+
var MAX_EMPTY_OUTPUT_RETRIES = 1;
|
|
3438
|
+
var activeChildren = /* @__PURE__ */ new Set();
|
|
3439
|
+
registerTestcaseRoutes(app);
|
|
3440
|
+
app.post("/api/chat/abort-all", (_req, res) => {
|
|
3441
|
+
let killed = 0;
|
|
3442
|
+
for (const child of activeChildren) {
|
|
3443
|
+
try {
|
|
3444
|
+
child.kill("SIGKILL");
|
|
3445
|
+
killed++;
|
|
3446
|
+
} catch {
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
res.json({ success: true, killed });
|
|
3450
|
+
});
|
|
3451
|
+
app.post("/api/chat", async (req, res) => {
|
|
3452
|
+
const { message, sessionId: existingSessionId, cwd, addDir, stage, clientId, appFacts, dataUpdated, config } = req.body;
|
|
3453
|
+
if (!message || typeof message !== "string") {
|
|
3454
|
+
res.status(400).json({ error: "message is required" });
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
const qoderPath = resolveExecutorCommand("qoder");
|
|
3458
|
+
if (!qoderPath) {
|
|
3459
|
+
res.status(503).json({ error: "Qoder CLI not installed" });
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
let sessionId = existingSessionId || randomUUID3();
|
|
3463
|
+
const workDir = cwd || sessions.get(sessionId)?.cwd || process.cwd();
|
|
3464
|
+
sessions.set(sessionId, { cwd: workDir });
|
|
3465
|
+
const isFirstTurn = !existingSessionId;
|
|
3466
|
+
const cliPath = findWfDeveloper() || getWfDeveloperPath() || "";
|
|
3467
|
+
const promptFile = await createSkillPromptFile(stage, config?.lang, fetchAppsCatalog, {
|
|
3468
|
+
cliPath,
|
|
3469
|
+
clientId: typeof clientId === "string" && clientId.trim() ? clientId.trim() : void 0,
|
|
3470
|
+
baseUrl: getBaseUrl(),
|
|
3471
|
+
appFacts: typeof appFacts === "string" && appFacts.trim() ? appFacts : void 0,
|
|
3472
|
+
dataUpdated: typeof dataUpdated === "string" && dataUpdated.trim() ? dataUpdated : void 0,
|
|
3473
|
+
// 工程根目录:优先本轮显式传入的 cwd(集成阶段前端透传 activeApp.cwd),用于替换 skill 占位符
|
|
3474
|
+
projectDir: typeof cwd === "string" && cwd.trim() ? cwd.trim() : void 0
|
|
3475
|
+
});
|
|
3476
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
3477
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
3478
|
+
res.setHeader("Connection", "keep-alive");
|
|
3479
|
+
res.flushHeaders?.();
|
|
3480
|
+
const sendSSE = (event, data) => {
|
|
3481
|
+
res.write(`event: ${event}
|
|
3482
|
+
`);
|
|
3483
|
+
res.write(`data: ${JSON.stringify(data)}
|
|
3484
|
+
|
|
3485
|
+
`);
|
|
3486
|
+
};
|
|
3487
|
+
sendSSE("meta", { cwd: workDir });
|
|
3488
|
+
let settled = false;
|
|
3489
|
+
let currentChild = null;
|
|
3490
|
+
const startAttempt = (attempt) => {
|
|
3491
|
+
let fullOutput = "";
|
|
3492
|
+
let lineBuffer = "";
|
|
3493
|
+
const segments = [];
|
|
3494
|
+
const toolSegById = /* @__PURE__ */ new Map();
|
|
3495
|
+
let sawStreamContent = false;
|
|
3496
|
+
const openSegment = (kind, initial = "") => {
|
|
3497
|
+
segments.push({ type: kind, content: initial });
|
|
3498
|
+
sendSSE("segment", { kind, index: segments.length - 1, name: kind === "tool" ? initial : void 0 });
|
|
3499
|
+
return segments.length - 1;
|
|
3500
|
+
};
|
|
3501
|
+
const appendToSegment = (kind, chunk) => {
|
|
3502
|
+
let index = segments.length - 1;
|
|
3503
|
+
if (index < 0 || segments[index].type !== kind) index = openSegment(kind);
|
|
3504
|
+
segments[index].content += chunk;
|
|
3505
|
+
sendSSE(kind, { chunk, index });
|
|
3506
|
+
};
|
|
3507
|
+
const args = ["--bare", "--setting-sources", "", "--print", "--output-format", "stream-json", "--input-format", "text", "--include-partial-messages", "--permission-mode", "bypass_permissions", isFirstTurn ? "--session-id" : "--resume", sessionId, "--cwd", workDir, "--append-system-prompt-file", promptFile];
|
|
3508
|
+
if (config?.model) args.push("--model", config.model);
|
|
3509
|
+
if (config?.reasoningEffort) args.push("--reasoning-effort", config.reasoningEffort);
|
|
3510
|
+
if (config?.contextWindow && Number.isInteger(config.contextWindow) && config.contextWindow > 0) args.push("--context-window", String(config.contextWindow));
|
|
3511
|
+
if (addDir) args.push("--add-dir", addDir);
|
|
3512
|
+
const child = spawn4(qoderPath, args, { cwd: workDir, env: { ...process.env }, stdio: ["pipe", "pipe", "pipe"] });
|
|
3513
|
+
currentChild = child;
|
|
3514
|
+
activeChildren.add(child);
|
|
3515
|
+
const processLine = (line) => {
|
|
3516
|
+
line = line.trim();
|
|
3517
|
+
if (!line) return;
|
|
3518
|
+
fullOutput += line + "\n";
|
|
3519
|
+
try {
|
|
3520
|
+
const obj = JSON.parse(line);
|
|
3521
|
+
if (obj.type === "stream_event") {
|
|
3522
|
+
const evt = obj.event || obj;
|
|
3523
|
+
if (evt.type === "content_block_start") {
|
|
3524
|
+
const blockType = evt.content_block?.type;
|
|
3525
|
+
if (blockType === "tool_use") {
|
|
3526
|
+
const toolName = evt.content_block.name || "unknown";
|
|
3527
|
+
sendSSE("tool", { tool: toolName });
|
|
3528
|
+
const idx = openSegment("tool", toolName);
|
|
3529
|
+
if (evt.content_block.id) toolSegById.set(evt.content_block.id, idx);
|
|
3530
|
+
} else if (blockType === "thinking" || blockType === "text") {
|
|
3531
|
+
openSegment(blockType);
|
|
3532
|
+
}
|
|
3533
|
+
} else if (evt.type === "content_block_delta" && evt.delta) {
|
|
3534
|
+
if (evt.delta.type === "thinking_delta" && evt.delta.thinking) {
|
|
3535
|
+
sawStreamContent = true;
|
|
3536
|
+
appendToSegment("thinking", evt.delta.thinking);
|
|
3537
|
+
} else if (evt.delta.type === "text_delta" && evt.delta.text) {
|
|
3538
|
+
sawStreamContent = true;
|
|
3539
|
+
appendToSegment("text", evt.delta.text);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
if (obj.type === "assistant" && obj.message?.content) {
|
|
3545
|
+
for (const block of obj.message.content) {
|
|
3546
|
+
if (block.type === "tool_use" && block.name) {
|
|
3547
|
+
let idx = block.id ? toolSegById.get(block.id) : void 0;
|
|
3548
|
+
if (idx === void 0) {
|
|
3549
|
+
idx = openSegment("tool", block.name);
|
|
3550
|
+
if (block.id) toolSegById.set(block.id, idx);
|
|
3551
|
+
sendSSE("tool", { tool: block.name });
|
|
3552
|
+
}
|
|
3553
|
+
const detail = summarizeToolInput(block.name, block.input);
|
|
3554
|
+
if (detail && detail !== segments[idx].detail) {
|
|
3555
|
+
segments[idx].detail = detail;
|
|
3556
|
+
sendSSE("tool_detail", { index: idx, detail });
|
|
3557
|
+
}
|
|
3558
|
+
} else if (!sawStreamContent) {
|
|
3559
|
+
if (block.type === "thinking" && block.thinking) {
|
|
3560
|
+
appendToSegment("thinking", block.thinking);
|
|
3561
|
+
} else if (block.type === "text" && block.text) {
|
|
3562
|
+
appendToSegment("text", block.text);
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
if (obj.type === "user" && obj.message?.content) {
|
|
3569
|
+
for (const block of obj.message.content) {
|
|
3570
|
+
if (block.type === "tool_result") {
|
|
3571
|
+
const idx = block.tool_use_id ? toolSegById.get(block.tool_use_id) : void 0;
|
|
3572
|
+
if (idx !== void 0) {
|
|
3573
|
+
const preview = extractToolResultText(block);
|
|
3574
|
+
if (preview && preview !== segments[idx].result) {
|
|
3575
|
+
segments[idx].result = preview;
|
|
3576
|
+
sendSSE("tool_detail", { index: idx, result: preview });
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
} catch {
|
|
3583
|
+
}
|
|
3584
|
+
};
|
|
3585
|
+
child.stdout.on("data", (chunk) => {
|
|
3586
|
+
lineBuffer += chunk.toString();
|
|
3587
|
+
const lines = lineBuffer.split("\n");
|
|
3588
|
+
lineBuffer = lines.pop() || "";
|
|
3589
|
+
for (const line of lines) processLine(line);
|
|
3590
|
+
});
|
|
3591
|
+
child.stderr.on("data", (chunk) => {
|
|
3592
|
+
const text = chunk.toString();
|
|
3593
|
+
lineBuffer += text;
|
|
3594
|
+
const lines = lineBuffer.split("\n");
|
|
3595
|
+
lineBuffer = lines.pop() || "";
|
|
3596
|
+
for (const line of lines) processLine(line);
|
|
3597
|
+
});
|
|
3598
|
+
child.stdin.write(message);
|
|
3599
|
+
child.stdin.end();
|
|
3600
|
+
const finish = (_code) => {
|
|
3601
|
+
if (settled) return;
|
|
3602
|
+
settled = true;
|
|
3603
|
+
activeChildren.delete(child);
|
|
3604
|
+
if (lineBuffer.trim()) processLine(lineBuffer);
|
|
3605
|
+
const parsed = parseAIOutput(fullOutput, sessionId);
|
|
3606
|
+
if (parsed.error === "empty_output" && attempt <= MAX_EMPTY_OUTPUT_RETRIES) {
|
|
3607
|
+
settled = false;
|
|
3608
|
+
if (isFirstTurn) {
|
|
3609
|
+
sessionId = randomUUID3();
|
|
3610
|
+
sessions.set(sessionId, { cwd: workDir });
|
|
3611
|
+
}
|
|
3612
|
+
startAttempt(attempt + 1);
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
if (parsed.error === "empty_output") {
|
|
3616
|
+
parsed.text = "\u26A0\uFE0F AI \u672C\u6B21\u672A\u8FD4\u56DE\u5185\u5BB9\uFF08\u4F1A\u8BDD\u521D\u59CB\u5316\u5F02\u5E38\uFF09\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\u91CD\u8BD5\u3002";
|
|
3617
|
+
parsed.error = void 0;
|
|
3618
|
+
}
|
|
3619
|
+
if (segments.length > 0) parsed.segments = segments;
|
|
3620
|
+
sendSSE("done", parsed);
|
|
3621
|
+
res.end();
|
|
3622
|
+
try {
|
|
3623
|
+
unlinkSync3(promptFile);
|
|
3624
|
+
} catch {
|
|
3625
|
+
}
|
|
3626
|
+
};
|
|
3627
|
+
child.on("error", () => {
|
|
3628
|
+
sendSSE("error", { message: "Process error" });
|
|
3629
|
+
finish(null);
|
|
3630
|
+
});
|
|
3631
|
+
child.on("close", (code) => finish(code));
|
|
3632
|
+
};
|
|
3633
|
+
startAttempt(1);
|
|
3634
|
+
res.on("close", () => {
|
|
3635
|
+
if (!settled && currentChild) {
|
|
3636
|
+
currentChild.kill("SIGKILL");
|
|
3637
|
+
}
|
|
3638
|
+
});
|
|
3639
|
+
});
|
|
3640
|
+
app.get("/api/session/check", (req, res) => {
|
|
3641
|
+
const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId.trim() : "";
|
|
3642
|
+
const cwd = typeof req.query.cwd === "string" ? req.query.cwd.trim() : "";
|
|
3643
|
+
if (!sessionId) {
|
|
3644
|
+
res.json({ success: true, data: { valid: false } });
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
const workDir = cwd || sessions.get(sessionId)?.cwd || process.cwd();
|
|
3648
|
+
let real = workDir;
|
|
3649
|
+
try {
|
|
3650
|
+
real = realpathSync2(workDir);
|
|
3651
|
+
} catch {
|
|
3652
|
+
}
|
|
3653
|
+
const slug = real.replace(/[\\/]+/g, "-");
|
|
3654
|
+
const base = join7(homedir(), ".qoder", "projects", slug);
|
|
3655
|
+
const valid = existsSync6(join7(base, `${sessionId}.jsonl`)) || existsSync6(join7(base, sessionId));
|
|
3656
|
+
res.json({ success: true, data: { valid } });
|
|
3657
|
+
});
|
|
3658
|
+
registerStoreRoutes(app);
|
|
3659
|
+
registerWebhookProxyRoutes(app);
|
|
3660
|
+
registerReleaseRoutes(app);
|
|
3661
|
+
registerLogsTailRoutes(app);
|
|
3662
|
+
registerLogsQueryRoutes(app);
|
|
3663
|
+
registerLocalToolRoutes(app);
|
|
3664
|
+
syncSkillsToQoder();
|
|
3665
|
+
function resolveDistDir() {
|
|
3666
|
+
for (const cand of [join7(__dirname4, "dist"), join7(__dirname4, "..", "dist")]) {
|
|
3667
|
+
if (existsSync6(join7(cand, "index.html"))) return cand;
|
|
3668
|
+
}
|
|
3669
|
+
return null;
|
|
3670
|
+
}
|
|
3671
|
+
var DIST = resolveDistDir();
|
|
3672
|
+
if (DIST) {
|
|
3673
|
+
app.use(
|
|
3674
|
+
express.static(DIST, {
|
|
3675
|
+
setHeaders: (res, filePath) => {
|
|
3676
|
+
if (filePath.endsWith("index.html")) res.setHeader("Cache-Control", "no-store");
|
|
3677
|
+
else if (/[\\/]assets[\\/]/.test(filePath)) res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
3678
|
+
}
|
|
3679
|
+
})
|
|
3680
|
+
);
|
|
3681
|
+
app.use((req, res, next) => {
|
|
3682
|
+
if (req.method === "GET" && !req.path.startsWith("/api") && !req.path.startsWith("/assets/")) {
|
|
3683
|
+
res.sendFile(join7(DIST, "index.html"));
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
next();
|
|
3687
|
+
});
|
|
3688
|
+
console.log(`[developer-console] serving frontend from ${DIST}`);
|
|
3689
|
+
} else {
|
|
3690
|
+
console.warn("[developer-console] dist/ \u672A\u627E\u5230\uFF0C\u524D\u7AEF\u9759\u6001\u6258\u7BA1\u672A\u542F\u7528\uFF08\u5F00\u53D1\u6A21\u5F0F\u8BF7\u7528 npm run dev \u7684 Vite \u524D\u7AEF\uFF09");
|
|
3691
|
+
}
|
|
3692
|
+
app.listen(PORT, "127.0.0.1", () => {
|
|
3693
|
+
console.log(`[developer-console] API server running at http://127.0.0.1:${PORT}`);
|
|
3694
|
+
});
|