feihong-code 0.6.1 → 7.2.0
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/.github/workflows/release.yml +173 -0
- package/CHANGELOG.md +83 -454
- package/dist/agent/browser-agent.js +354 -0
- package/dist/agent/browser-agent.js.map +1 -0
- package/dist/agent/change-manager.js +433 -0
- package/dist/agent/change-manager.js.map +1 -0
- package/dist/agent/code-rag.js +293 -0
- package/dist/agent/code-rag.js.map +1 -0
- package/dist/agent/code-writer.js +13 -1
- package/dist/agent/code-writer.js.map +1 -1
- package/dist/agent/completion-engine.js +522 -0
- package/dist/agent/completion-engine.js.map +1 -0
- package/dist/agent/context-compactor.js +188 -10
- package/dist/agent/context-compactor.js.map +1 -1
- package/dist/agent/custom-agent.js +368 -0
- package/dist/agent/custom-agent.js.map +1 -0
- package/dist/agent/design-to-code.js +218 -0
- package/dist/agent/design-to-code.js.map +1 -0
- package/dist/agent/event-driven-agent.js +359 -0
- package/dist/agent/event-driven-agent.js.map +1 -0
- package/dist/agent/git-integration.js +324 -0
- package/dist/agent/git-integration.js.map +1 -0
- package/dist/agent/layered-memory.js +329 -0
- package/dist/agent/layered-memory.js.map +1 -0
- package/dist/agent/multi-agent.js +359 -0
- package/dist/agent/multi-agent.js.map +1 -0
- package/dist/agent/orchestrator.js +160 -22
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/prompts.js +20 -0
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/quality-gate.js +139 -0
- package/dist/agent/quality-gate.js.map +1 -1
- package/dist/agent/self-correction.js +274 -0
- package/dist/agent/self-correction.js.map +1 -0
- package/dist/agent/self-heal.js +42 -12
- package/dist/agent/self-heal.js.map +1 -1
- package/dist/agent/solo-agent.js +321 -0
- package/dist/agent/solo-agent.js.map +1 -0
- package/dist/agent/symbol-index.js +258 -28
- package/dist/agent/symbol-index.js.map +1 -1
- package/dist/agent/team-collaboration.js +252 -0
- package/dist/agent/team-collaboration.js.map +1 -0
- package/dist/agent/type-checker.js +229 -0
- package/dist/agent/type-checker.js.map +1 -0
- package/dist/cli/run.js +1 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/integrations/collaboration.js +219 -0
- package/dist/integrations/collaboration.js.map +1 -0
- package/dist/integrations/figma.js +307 -0
- package/dist/integrations/figma.js.map +1 -0
- package/dist/integrations/sso.js +327 -0
- package/dist/integrations/sso.js.map +1 -0
- package/dist/knowledge/library.js +301 -0
- package/dist/knowledge/library.js.map +1 -0
- package/dist/plugins/manager.js +332 -0
- package/dist/plugins/manager.js.map +1 -0
- package/dist/runtime/event-log.js.map +1 -1
- package/dist/tools/browser/browser.tool.js +217 -0
- package/dist/tools/browser/browser.tool.js.map +1 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/shell/exec.js +50 -10
- package/dist/tools/shell/exec.js.map +1 -1
- package/dist/tools/shell/run-shell.tool.js +8 -2
- package/dist/tools/shell/run-shell.tool.js.map +1 -1
- package/dist/training/completion-evaluator.js +213 -0
- package/dist/training/completion-evaluator.js.map +1 -0
- package/dist/training/fim-data.js +248 -0
- package/dist/training/fim-data.js.map +1 -0
- package/dist/voice/voice-programming.js +223 -0
- package/dist/voice/voice-programming.js.map +1 -0
- package/dist/web/public/css/style.css +190 -7
- package/dist/web/public/index.html +403 -9
- package/dist/web/public/js/api.js +191 -4
- package/dist/web/public/js/app.js +850 -123
- package/dist/web/public/js/mock-api.js +234 -0
- package/dist/web/public/js/monaco-editor.js +203 -0
- package/dist/web/public/js/ui.js +1271 -54
- package/dist/web/public/manifest.json +59 -0
- package/dist/web/public/service-worker.js +173 -0
- package/dist/web/public/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
- package/dist/web/public/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/dist/web/public/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
- package/dist/web/public/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
- package/dist/web/public/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
- package/dist/web/public/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
- package/dist/web/public/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
- package/dist/web/public/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
- package/dist/web/public/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/dist/web/public/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/dist/web/public/vendor/monaco/vs/bat-C397hTD6.js +1 -0
- package/dist/web/public/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/dist/web/public/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/dist/web/public/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/dist/web/public/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/dist/web/public/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/dist/web/public/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/dist/web/public/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/dist/web/public/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
- package/dist/web/public/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
- package/dist/web/public/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
- package/dist/web/public/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/dist/web/public/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/dist/web/public/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/dist/web/public/vendor/monaco/vs/ecl-30fUercY.js +1 -0
- package/dist/web/public/vendor/monaco/vs/editor/editor.main.css +1 -0
- package/dist/web/public/vendor/monaco/vs/editor/editor.main.js +1 -0
- package/dist/web/public/vendor/monaco/vs/editor/editor.worker.js +1 -0
- package/dist/web/public/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
- package/dist/web/public/vendor/monaco/vs/editor.js +1 -0
- package/dist/web/public/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
- package/dist/web/public/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/dist/web/public/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/dist/web/public/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
- package/dist/web/public/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/dist/web/public/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/dist/web/public/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/dist/web/public/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
- package/dist/web/public/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/dist/web/public/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
- package/dist/web/public/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
- package/dist/web/public/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
- package/dist/web/public/vendor/monaco/vs/index-049xrXrp.js +1 -0
- package/dist/web/public/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
- package/dist/web/public/vendor/monaco/vs/index.js +1 -0
- package/dist/web/public/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
- package/dist/web/public/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
- package/dist/web/public/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/dist/web/public/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
- package/dist/web/public/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
- package/dist/web/public/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
- package/dist/web/public/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/dist/web/public/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/dist/web/public/vendor/monaco/vs/language/css/css.worker.js +66 -0
- package/dist/web/public/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/dist/web/public/vendor/monaco/vs/language/html/html.worker.js +443 -0
- package/dist/web/public/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/dist/web/public/vendor/monaco/vs/language/json/json.worker.js +17 -0
- package/dist/web/public/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/dist/web/public/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/dist/web/public/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
- package/dist/web/public/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
- package/dist/web/public/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/dist/web/public/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
- package/dist/web/public/vendor/monaco/vs/loader.js +1368 -0
- package/dist/web/public/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
- package/dist/web/public/vendor/monaco/vs/lua-DtygF91M.js +1 -0
- package/dist/web/public/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/dist/web/public/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
- package/dist/web/public/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
- package/dist/web/public/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/dist/web/public/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
- package/dist/web/public/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
- package/dist/web/public/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
- package/dist/web/public/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
- package/dist/web/public/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
- package/dist/web/public/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
- package/dist/web/public/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/dist/web/public/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/cs.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/cs.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/de.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/de.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/es.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/es.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/fr.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/fr.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/it.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/it.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/ja.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/ja.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/ko.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/ko.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/pl.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/pl.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/pt-br.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/ru.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/ru.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/tr.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/tr.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.d.ts +1 -0
- package/dist/web/public/vendor/monaco/vs/nls/lang/zh-tw.js +9 -0
- package/dist/web/public/vendor/monaco/vs/nls.messages-loader.js +1 -0
- package/dist/web/public/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/dist/web/public/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/dist/web/public/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/dist/web/public/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
- package/dist/web/public/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/dist/web/public/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
- package/dist/web/public/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
- package/dist/web/public/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/dist/web/public/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/dist/web/public/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/dist/web/public/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/dist/web/public/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/dist/web/public/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
- package/dist/web/public/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/dist/web/public/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
- package/dist/web/public/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
- package/dist/web/public/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/dist/web/public/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/dist/web/public/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/dist/web/public/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/dist/web/public/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
- package/dist/web/public/vendor/monaco/vs/sb-3GYllVck.js +1 -0
- package/dist/web/public/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
- package/dist/web/public/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/dist/web/public/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/dist/web/public/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/dist/web/public/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/dist/web/public/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/dist/web/public/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/dist/web/public/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
- package/dist/web/public/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/dist/web/public/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
- package/dist/web/public/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/dist/web/public/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/dist/web/public/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
- package/dist/web/public/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
- package/dist/web/public/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
- package/dist/web/public/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
- package/dist/web/public/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
- package/dist/web/public/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
- package/dist/web/public/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
- package/dist/web/public/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/dist/web/public/vendor/monaco/vs/workers-BBttULjf.js +1 -0
- package/dist/web/public/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
- package/dist/web/public/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
- package/dist/web/server.js +2073 -210
- package/dist/web/server.js.map +1 -1
- package/dist/web/task-queue.js +25 -5
- package/dist/web/task-queue.js.map +1 -1
- package/docs/BENCHMARK_REPORT_en.md +329 -0
- package/docs/BENCHMARK_REPORT_zh.md +329 -0
- package/docs/DEPLOYMENT.md +382 -0
- package/docs/MODEL_FINETUNING.md +405 -0
- package/docs/TECHNICAL_SPEC_en.md +292 -0
- package/docs/TECHNICAL_SPEC_zh.md +292 -0
- package/docs/UPDATE_NOTES_en.md +159 -0
- package/docs/UPDATE_NOTES_zh.md +159 -0
- package/electron/main.js +602 -0
- package/package.json +67 -3
- package/tool-schema.json +1 -1
package/dist/web/server.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -29,6 +62,19 @@ const version_1 = require("../cli/version");
|
|
|
29
62
|
const i18n_1 = require("../shared/i18n");
|
|
30
63
|
const enterprise_1 = require("../enterprise");
|
|
31
64
|
const config_1 = require("../shared/config");
|
|
65
|
+
const model_router_1 = require("../models/model-router");
|
|
66
|
+
const openai_compatible_provider_1 = require("../models/providers/openai-compatible.provider");
|
|
67
|
+
const completion_engine_1 = require("../agent/completion-engine");
|
|
68
|
+
const symbol_index_1 = require("../agent/symbol-index");
|
|
69
|
+
const change_manager_1 = require("../agent/change-manager");
|
|
70
|
+
const design_to_code_1 = require("../agent/design-to-code");
|
|
71
|
+
const git_integration_1 = require("../agent/git-integration");
|
|
72
|
+
const team_collaboration_1 = require("../agent/team-collaboration");
|
|
73
|
+
const solo_agent_1 = require("../agent/solo-agent");
|
|
74
|
+
const multi_agent_1 = require("../agent/multi-agent");
|
|
75
|
+
const event_driven_agent_1 = require("../agent/event-driven-agent");
|
|
76
|
+
const custom_agent_1 = require("../agent/custom-agent");
|
|
77
|
+
const mcp_client_1 = require("../tools/mcp/mcp-client");
|
|
32
78
|
const memory_1 = require("../memory");
|
|
33
79
|
const auto_summarize_1 = require("../memory/auto-summarize");
|
|
34
80
|
const secure_store_1 = require("../shared/secure-store");
|
|
@@ -58,6 +104,22 @@ function startWebServer(opts = {}) {
|
|
|
58
104
|
const sessions = new auth_1.SessionStore();
|
|
59
105
|
// 当前 Web 控制台工作区,任务提交缺省时使用
|
|
60
106
|
let serverWorkspaceDir = (0, path_1.resolve)(process.cwd());
|
|
107
|
+
// P3: 多文件变更管理器(AI 生成的修改先暂存,用户审批后才写入磁盘)
|
|
108
|
+
const changeManager = new change_manager_1.ChangeManager({ cwd: serverWorkspaceDir });
|
|
109
|
+
// P2-2: Git 集成管理器
|
|
110
|
+
let gitIntegration = (0, git_integration_1.createGitIntegration)(serverWorkspaceDir);
|
|
111
|
+
// P2-3: 团队协作管理器
|
|
112
|
+
const teamManager = (0, team_collaboration_1.createTeamCollaborationManager)();
|
|
113
|
+
// 阶段二-2: 事件驱动 Agent 管理器
|
|
114
|
+
const eventDrivenManager = (0, event_driven_agent_1.createEventDrivenAgentManager)(serverWorkspaceDir, async (event) => {
|
|
115
|
+
console.log('[event-driven] processing:', { eventId: event.id, type: event.type });
|
|
116
|
+
// 实际处理逻辑:根据事件类型触发对应的 Agent 任务
|
|
117
|
+
// 这里简化处理,实际应集成到 orchestrator
|
|
118
|
+
});
|
|
119
|
+
// 阶段二-3: 自定义 Agent 管理器
|
|
120
|
+
const customAgentManager = (0, custom_agent_1.createCustomAgentManager)(serverWorkspaceDir);
|
|
121
|
+
// P3-2: SOLO 全自主编程任务存储
|
|
122
|
+
const soloTasks = new Map();
|
|
61
123
|
// 任务队列(进程内;服务端静默执行)— 需在登录接口前初始化
|
|
62
124
|
const persistDir = process.env.FH_TASK_PERSIST_DIR?.trim() ||
|
|
63
125
|
(0, path_1.join)(process.env.FH_HOME?.trim() || (0, path_1.join)(require('os').homedir(), '.feihong-code'), 'tasks');
|
|
@@ -65,6 +127,13 @@ function startWebServer(opts = {}) {
|
|
|
65
127
|
concurrency: Number(process.env.FH_TASK_CONCURRENCY ?? 2),
|
|
66
128
|
webhookUrl: process.env.FH_TASK_WEBHOOK_URL,
|
|
67
129
|
persistDir,
|
|
130
|
+
// P3-1: AI 生成的文件修改自动暂存到变更面板
|
|
131
|
+
stageChange: (path, content) => {
|
|
132
|
+
try {
|
|
133
|
+
changeManager.stageChange(path, content);
|
|
134
|
+
}
|
|
135
|
+
catch (e) { /* 暂存失败不影响任务执行 */ }
|
|
136
|
+
},
|
|
68
137
|
});
|
|
69
138
|
// 公开健康检查(仅暴露版本/状态等观测信息,无敏感数据)
|
|
70
139
|
app.get('/api/health', (_req, res) => {
|
|
@@ -129,10 +198,58 @@ function startWebServer(opts = {}) {
|
|
|
129
198
|
const content = (0, memory_1.readShortTerm)(memoryConfig, date ? new Date(date) : undefined);
|
|
130
199
|
res.json({ ok: true, content, date: date || new Date().toISOString().split('T')[0] });
|
|
131
200
|
});
|
|
201
|
+
// 手动添加一条短期记忆
|
|
202
|
+
app.post('/api/memory/short', (req, res) => {
|
|
203
|
+
const body = (req.body ?? {});
|
|
204
|
+
const type = ['task', 'fix', 'feature', 'error', 'note'].find((t) => t === body.type) || 'note';
|
|
205
|
+
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : '';
|
|
206
|
+
const content = typeof body.content === 'string' && body.content.trim() ? body.content.trim() : '';
|
|
207
|
+
if (!title || !content) {
|
|
208
|
+
res.status(400).json({ ok: false, error: '缺少 title 或 content 字段' });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const path = (0, memory_1.appendShortTerm)(memoryConfig, { type, title, content });
|
|
213
|
+
res.json({ ok: true, path, message: '已添加到短期记忆' });
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
res.status(500).json({ ok: false, error: '添加失败: ' + e.message });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
132
219
|
app.get('/api/memory/long', (_req, res) => {
|
|
133
220
|
const content = (0, memory_1.readLongTerm)(memoryConfig);
|
|
134
221
|
res.json({ ok: true, content });
|
|
135
222
|
});
|
|
223
|
+
// 写入/编辑长期记忆(用户自定义需要记忆的内容)
|
|
224
|
+
app.post('/api/memory/long', (req, res) => {
|
|
225
|
+
const body = (req.body ?? {});
|
|
226
|
+
const content = typeof body.content === 'string' ? body.content : '';
|
|
227
|
+
try {
|
|
228
|
+
(0, memory_1.writeLongTerm)(memoryConfig, content);
|
|
229
|
+
res.json({ ok: true, message: '长期记忆已保存' });
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
res.status(500).json({ ok: false, error: '保存失败: ' + e.message });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// 追加一条长期记忆
|
|
236
|
+
app.post('/api/memory/long/append', (req, res) => {
|
|
237
|
+
const body = (req.body ?? {});
|
|
238
|
+
const category = typeof body.category === 'string' && body.category.trim() ? body.category.trim() : '自定义';
|
|
239
|
+
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : '';
|
|
240
|
+
const content = typeof body.content === 'string' && body.content.trim() ? body.content.trim() : '';
|
|
241
|
+
if (!title || !content) {
|
|
242
|
+
res.status(400).json({ ok: false, error: '缺少 title 或 content 字段' });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const id = (0, memory_1.appendLongTerm)(memoryConfig, { category, title, content, summarizedFrom: '手动添加' });
|
|
247
|
+
res.json({ ok: true, id, message: '已追加到长期记忆' });
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
res.status(500).json({ ok: false, error: '追加失败: ' + e.message });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
136
253
|
app.get('/api/memory/stats', (_req, res) => {
|
|
137
254
|
const stats = (0, memory_1.getMemoryStats)(memoryConfig);
|
|
138
255
|
res.json({ ok: true, ...stats });
|
|
@@ -402,6 +519,70 @@ function startWebServer(opts = {}) {
|
|
|
402
519
|
res.status(500).json({ ok: false, error: '读取目录失败: ' + e.message });
|
|
403
520
|
}
|
|
404
521
|
});
|
|
522
|
+
// 新建文件夹
|
|
523
|
+
app.post('/api/workspace/mkdir', (req, res) => {
|
|
524
|
+
const body = (req.body ?? {});
|
|
525
|
+
const parent = typeof body?.parent === 'string' ? body.parent.trim() : '';
|
|
526
|
+
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
527
|
+
if (!parent || !name) {
|
|
528
|
+
res.status(400).json({ ok: false, error: '缺少 parent 或 name 字段' });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
// 文件夹名安全校验:禁止路径分隔符和特殊字符
|
|
532
|
+
if (/[\\/:*?"<>|]/.test(name)) {
|
|
533
|
+
res.status(400).json({ ok: false, error: '文件夹名包含非法字符' });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const parentDir = (0, path_1.resolve)(parent);
|
|
537
|
+
if (!assertPathAllowed(parentDir, res))
|
|
538
|
+
return;
|
|
539
|
+
const newDir = (0, path_1.join)(parentDir, name);
|
|
540
|
+
try {
|
|
541
|
+
if ((0, fs_1.existsSync)(newDir)) {
|
|
542
|
+
res.status(409).json({ ok: false, error: '文件夹已存在' });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
(0, fs_1.mkdirSync)(newDir, { recursive: true });
|
|
546
|
+
res.json({ ok: true, path: newDir });
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
res.status(500).json({ ok: false, error: '创建文件夹失败: ' + e.message });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
// 重命名文件夹
|
|
553
|
+
app.post('/api/workspace/rename', (req, res) => {
|
|
554
|
+
const body = (req.body ?? {});
|
|
555
|
+
const path = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
556
|
+
const newName = typeof body?.newName === 'string' ? body.newName.trim() : '';
|
|
557
|
+
if (!path || !newName) {
|
|
558
|
+
res.status(400).json({ ok: false, error: '缺少 path 或 newName 字段' });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (/[\\/:*?"<>|]/.test(newName)) {
|
|
562
|
+
res.status(400).json({ ok: false, error: '文件夹名包含非法字符' });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const oldPath = (0, path_1.resolve)(path);
|
|
566
|
+
if (!assertPathAllowed(oldPath, res))
|
|
567
|
+
return;
|
|
568
|
+
if (!(0, fs_1.existsSync)(oldPath) || !(0, fs_1.statSync)(oldPath).isDirectory()) {
|
|
569
|
+
res.status(400).json({ ok: false, error: '目标不是文件夹或不存在' });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const parentDir = (0, path_1.dirname)(oldPath);
|
|
573
|
+
const newPath = (0, path_1.join)(parentDir, newName);
|
|
574
|
+
try {
|
|
575
|
+
if ((0, fs_1.existsSync)(newPath)) {
|
|
576
|
+
res.status(409).json({ ok: false, error: '同名文件夹已存在' });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
(0, fs_1.renameSync)(oldPath, newPath);
|
|
580
|
+
res.json({ ok: true, path: newPath });
|
|
581
|
+
}
|
|
582
|
+
catch (e) {
|
|
583
|
+
res.status(500).json({ ok: false, error: '重命名失败: ' + e.message });
|
|
584
|
+
}
|
|
585
|
+
});
|
|
405
586
|
app.post('/api/files/read', (req, res) => {
|
|
406
587
|
const body = (req.body ?? {});
|
|
407
588
|
const file = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
@@ -424,207 +605,1752 @@ function startWebServer(opts = {}) {
|
|
|
424
605
|
res.status(500).json({ ok: false, error: '读取失败: ' + e.message });
|
|
425
606
|
}
|
|
426
607
|
});
|
|
427
|
-
/* ==========
|
|
428
|
-
|
|
608
|
+
/* ========== P3: 多文件变更管理(暂存/审批/原子提交) ========== */
|
|
609
|
+
// 暂存变更(AI 生成的文件内容先暂存,不直接写入)
|
|
610
|
+
app.post('/api/changes/stage', (req, res) => {
|
|
429
611
|
const body = (req.body ?? {});
|
|
430
|
-
const
|
|
431
|
-
|
|
612
|
+
const path = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
613
|
+
const content = typeof body?.content === 'string' ? body.content : '';
|
|
614
|
+
if (!path) {
|
|
615
|
+
res.status(400).json({ ok: false, error: '缺少 path 字段' });
|
|
432
616
|
return;
|
|
433
|
-
try {
|
|
434
|
-
openFolder(dir);
|
|
435
|
-
res.json({ ok: true });
|
|
436
617
|
}
|
|
437
|
-
|
|
438
|
-
|
|
618
|
+
const change = changeManager.stageChange(path, content);
|
|
619
|
+
if (!change) {
|
|
620
|
+
res.status(400).json({ ok: false, error: '内容未变化或暂存区已满' });
|
|
621
|
+
return;
|
|
439
622
|
}
|
|
623
|
+
res.json({ ok: true, change: { path: change.path, type: change.type, additions: change.additions, deletions: change.deletions, status: change.status, hunks: change.hunks } });
|
|
440
624
|
});
|
|
441
|
-
|
|
625
|
+
// 获取所有暂存变更(变更面板数据)
|
|
626
|
+
app.get('/api/changes', (_req, res) => {
|
|
627
|
+
res.json({ ok: true, ...changeManager.toPanelData() });
|
|
628
|
+
});
|
|
629
|
+
// 接受整个文件的变更
|
|
630
|
+
app.post('/api/changes/accept', (req, res) => {
|
|
442
631
|
const body = (req.body ?? {});
|
|
443
|
-
const
|
|
444
|
-
if (!
|
|
445
|
-
res.status(400).json({ ok: false, error: '缺少
|
|
632
|
+
const path = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
633
|
+
if (!path) {
|
|
634
|
+
res.status(400).json({ ok: false, error: '缺少 path 字段' });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const ok = changeManager.acceptFile(path);
|
|
638
|
+
res.json({ ok, ...(ok ? {} : { error: '文件不存在' }) });
|
|
639
|
+
});
|
|
640
|
+
// 拒绝整个文件的变更
|
|
641
|
+
app.post('/api/changes/reject', (req, res) => {
|
|
642
|
+
const body = (req.body ?? {});
|
|
643
|
+
const path = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
644
|
+
if (!path) {
|
|
645
|
+
res.status(400).json({ ok: false, error: '缺少 path 字段' });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const ok = changeManager.rejectFile(path);
|
|
649
|
+
res.json({ ok, ...(ok ? {} : { error: '文件不存在' }) });
|
|
650
|
+
});
|
|
651
|
+
// 接受单个 Hunk
|
|
652
|
+
app.post('/api/changes/hunk/accept', (req, res) => {
|
|
653
|
+
const body = (req.body ?? {});
|
|
654
|
+
const path = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
655
|
+
const hunkIndex = typeof body?.hunkIndex === 'number' ? body.hunkIndex : -1;
|
|
656
|
+
if (!path || hunkIndex < 0) {
|
|
657
|
+
res.status(400).json({ ok: false, error: '缺少 path 或 hunkIndex' });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const ok = changeManager.acceptHunk(path, hunkIndex);
|
|
661
|
+
res.json({ ok, ...(ok ? {} : { error: '文件或 Hunk 不存在' }) });
|
|
662
|
+
});
|
|
663
|
+
// 拒绝单个 Hunk
|
|
664
|
+
app.post('/api/changes/hunk/reject', (req, res) => {
|
|
665
|
+
const body = (req.body ?? {});
|
|
666
|
+
const path = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
667
|
+
const hunkIndex = typeof body?.hunkIndex === 'number' ? body.hunkIndex : -1;
|
|
668
|
+
if (!path || hunkIndex < 0) {
|
|
669
|
+
res.status(400).json({ ok: false, error: '缺少 path 或 hunkIndex' });
|
|
446
670
|
return;
|
|
447
671
|
}
|
|
672
|
+
const ok = changeManager.rejectHunk(path, hunkIndex);
|
|
673
|
+
res.json({ ok, ...(ok ? {} : { error: '文件或 Hunk 不存在' }) });
|
|
674
|
+
});
|
|
675
|
+
// 原子化提交(写入所有已接受的变更,失败自动回滚)
|
|
676
|
+
app.post('/api/changes/commit', (_req, res) => {
|
|
677
|
+
const result = changeManager.commit();
|
|
678
|
+
res.json({ ok: result.success, ...result });
|
|
679
|
+
});
|
|
680
|
+
// 丢弃所有暂存变更
|
|
681
|
+
app.post('/api/changes/discard', (_req, res) => {
|
|
682
|
+
changeManager.discardAll();
|
|
683
|
+
res.json({ ok: true });
|
|
684
|
+
});
|
|
685
|
+
// 检测冲突
|
|
686
|
+
app.post('/api/changes/detect-conflicts', (_req, res) => {
|
|
687
|
+
const conflicts = changeManager.detectConflicts();
|
|
688
|
+
res.json({ ok: true, conflicts });
|
|
689
|
+
});
|
|
690
|
+
/* ========== P1-2: MCP 协议管理 API ========== */
|
|
691
|
+
// 获取当前 MCP 服务器配置
|
|
692
|
+
app.get('/api/mcp/config', (_req, res) => {
|
|
448
693
|
try {
|
|
449
|
-
|
|
450
|
-
|
|
694
|
+
const cfg = (0, config_1.loadConfig)();
|
|
695
|
+
const servers = (cfg.mcp?.servers ?? []).map((s) => ({
|
|
696
|
+
name: s.name,
|
|
697
|
+
command: s.command,
|
|
698
|
+
args: s.args ?? [],
|
|
699
|
+
env: s.env ? Object.keys(s.env) : [], // 只暴露环境变量名,不暴露值
|
|
700
|
+
}));
|
|
701
|
+
res.json({ ok: true, servers, count: servers.length });
|
|
451
702
|
}
|
|
452
703
|
catch (e) {
|
|
453
|
-
res.status(500).json({ ok: false, error:
|
|
704
|
+
res.status(500).json({ ok: false, error: `读取配置失败: ${e instanceof Error ? e.message : String(e)}` });
|
|
454
705
|
}
|
|
455
706
|
});
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
app.post('/api/upload', (req, res) => {
|
|
707
|
+
// 测试指定 MCP 服务器连接(临时连接,列出工具后关闭)
|
|
708
|
+
app.post('/api/mcp/test', async (req, res) => {
|
|
459
709
|
const body = (req.body ?? {});
|
|
460
710
|
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (!name || !data) {
|
|
464
|
-
res.status(400).json({ ok: false, error: '缺少 name 或 dataBase64 字段' });
|
|
711
|
+
if (!name) {
|
|
712
|
+
res.status(400).json({ ok: false, error: '缺少 name 字段' });
|
|
465
713
|
return;
|
|
466
714
|
}
|
|
467
715
|
try {
|
|
468
|
-
(0,
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
716
|
+
const cfg = (0, config_1.loadConfig)();
|
|
717
|
+
const serverCfg = (cfg.mcp?.servers ?? []).find((s) => s.name === name);
|
|
718
|
+
if (!serverCfg) {
|
|
719
|
+
res.status(404).json({ ok: false, error: `未找到 MCP 服务器: ${name}` });
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const startTime = Date.now();
|
|
723
|
+
const client = await (0, mcp_client_1.connectMcp)(serverCfg);
|
|
724
|
+
const tools = await client.listTools();
|
|
725
|
+
const latencyMs = Date.now() - startTime;
|
|
726
|
+
await client.close().catch(() => undefined);
|
|
727
|
+
res.json({
|
|
728
|
+
ok: true,
|
|
729
|
+
name,
|
|
730
|
+
connected: true,
|
|
731
|
+
latencyMs,
|
|
732
|
+
toolCount: tools.length,
|
|
733
|
+
tools: tools.map((t) => ({
|
|
734
|
+
name: t.name,
|
|
735
|
+
description: (t.description || '').slice(0, 100),
|
|
736
|
+
})),
|
|
737
|
+
});
|
|
473
738
|
}
|
|
474
739
|
catch (e) {
|
|
475
|
-
res.
|
|
740
|
+
res.json({
|
|
741
|
+
ok: false,
|
|
742
|
+
name,
|
|
743
|
+
connected: false,
|
|
744
|
+
error: `连接失败: ${e instanceof Error ? e.message : String(e)}`,
|
|
745
|
+
});
|
|
476
746
|
}
|
|
477
747
|
});
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const AGENT_FOUNDRY_CATALOG = 'https://raw.githubusercontent.com/hebertzhu/agent-foundry/main/catalog/skills-catalog.json';
|
|
481
|
-
async function fetchWithRetry(url, timeoutMs = 15000, retries = 1) {
|
|
482
|
-
let lastErr;
|
|
483
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
484
|
-
const ctrl = new AbortController();
|
|
485
|
-
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
486
|
-
try {
|
|
487
|
-
const r = await fetch(url, {
|
|
488
|
-
signal: ctrl.signal,
|
|
489
|
-
headers: { 'User-Agent': 'fhcode-web/0.5.1' },
|
|
490
|
-
});
|
|
491
|
-
clearTimeout(timer);
|
|
492
|
-
return r;
|
|
493
|
-
}
|
|
494
|
-
catch (e) {
|
|
495
|
-
clearTimeout(timer);
|
|
496
|
-
lastErr = e;
|
|
497
|
-
if (attempt < retries)
|
|
498
|
-
await new Promise((res) => setTimeout(res, 500));
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
throw lastErr;
|
|
502
|
-
}
|
|
503
|
-
async function fetchClawHubSkills(keyword, limit) {
|
|
748
|
+
// 列出所有 MCP 服务器的工具(逐个连接测试,返回汇总)
|
|
749
|
+
app.get('/api/mcp/tools', async (_req, res) => {
|
|
504
750
|
try {
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
homepage: `https://clawhub.ai/${String(it.slug || '')}`,
|
|
521
|
-
rawUrl: `https://clawhub.ai/${String(it.slug || '')}`,
|
|
522
|
-
}));
|
|
751
|
+
const cfg = (0, config_1.loadConfig)();
|
|
752
|
+
const servers = cfg.mcp?.servers ?? [];
|
|
753
|
+
const results = [];
|
|
754
|
+
for (const s of servers) {
|
|
755
|
+
try {
|
|
756
|
+
const client = await (0, mcp_client_1.connectMcp)(s);
|
|
757
|
+
const tools = await client.listTools();
|
|
758
|
+
await client.close().catch(() => undefined);
|
|
759
|
+
results.push({ name: s.name, ok: true, toolCount: tools.length });
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
results.push({ name: s.name, ok: false, toolCount: 0, error: e instanceof Error ? e.message : String(e) });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
res.json({ ok: true, servers: results, totalServers: servers.length });
|
|
523
766
|
}
|
|
524
767
|
catch (e) {
|
|
525
|
-
|
|
526
|
-
|
|
768
|
+
res.status(500).json({ ok: false, error: `获取 MCP 工具失败: ${e instanceof Error ? e.message : String(e)}` });
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
/* ========== P2: 代码补全 API(真实接入 CompletionEngine) ========== */
|
|
772
|
+
app.post('/api/completion', async (req, res) => {
|
|
773
|
+
const body = (req.body ?? {});
|
|
774
|
+
const filePath = typeof body?.filePath === 'string' ? body.filePath.trim() : '';
|
|
775
|
+
const fileContent = typeof body?.fileContent === 'string' ? body.fileContent : '';
|
|
776
|
+
const cursorOffset = typeof body?.cursorOffset === 'number' ? body.cursorOffset : fileContent.length;
|
|
777
|
+
const mode = body?.mode === 'full' ? 'full' : 'quick';
|
|
778
|
+
const language = typeof body?.language === 'string' ? body.language : undefined;
|
|
779
|
+
if (!filePath || !fileContent) {
|
|
780
|
+
res.status(400).json({ ok: false, error: '缺少 filePath 或 fileContent' });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (!completionEngine) {
|
|
784
|
+
res.json({ ok: true, suggestions: [], latencyMs: 0, model: '', cached: false, note: '未配置模型,补全不可用' });
|
|
785
|
+
return;
|
|
527
786
|
}
|
|
528
|
-
}
|
|
529
|
-
async function fetchAgentFoundrySkills(keyword, limit) {
|
|
530
787
|
try {
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const name = String(it.name || '');
|
|
538
|
-
const publicPath = String(it.public_path || '');
|
|
539
|
-
const category = String(it.category || 'market');
|
|
540
|
-
const rawUrl = publicPath
|
|
541
|
-
? `https://raw.githubusercontent.com/hebertzhu/agent-foundry/main/${publicPath}`
|
|
542
|
-
: `https://github.com/hebertzhu/agent-foundry`;
|
|
543
|
-
return {
|
|
544
|
-
id: `agent-foundry:${name}`,
|
|
545
|
-
name,
|
|
546
|
-
description: `Agent-Foundry 技能包 · 分类:${category}${publicPath ? ` · 源:${publicPath}` : ''}`,
|
|
547
|
-
source: 'agent-foundry',
|
|
548
|
-
author: 'hebertzhu',
|
|
549
|
-
category,
|
|
550
|
-
tags: Array.isArray(it.packs) ? it.packs : [category],
|
|
551
|
-
downloads: 0,
|
|
552
|
-
installHint: `npm i -g @agent-foundry/cli && agent-foundry install ${name}`,
|
|
553
|
-
homepage: `https://github.com/hebertzhu/agent-foundry`,
|
|
554
|
-
rawUrl,
|
|
555
|
-
};
|
|
788
|
+
const result = await completionEngine.complete({
|
|
789
|
+
filePath,
|
|
790
|
+
fileContent,
|
|
791
|
+
cursorOffset,
|
|
792
|
+
mode,
|
|
793
|
+
language,
|
|
556
794
|
});
|
|
557
|
-
|
|
558
|
-
const kw = keyword.toLowerCase();
|
|
559
|
-
list = list.filter((s) => s.name.toLowerCase().includes(kw) ||
|
|
560
|
-
s.description.toLowerCase().includes(kw) ||
|
|
561
|
-
(s.tags || []).some((t) => t.toLowerCase().includes(kw)));
|
|
562
|
-
}
|
|
563
|
-
return list.slice(0, limit);
|
|
795
|
+
res.json({ ok: true, ...result });
|
|
564
796
|
}
|
|
565
797
|
catch (e) {
|
|
566
|
-
|
|
567
|
-
return [];
|
|
798
|
+
res.status(500).json({ ok: false, error: '补全失败: ' + e.message, suggestions: [] });
|
|
568
799
|
}
|
|
569
|
-
}
|
|
570
|
-
app.get('/api/skills/market', async (req, res) => {
|
|
571
|
-
const q = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
572
|
-
const source = typeof req.query.source === 'string' ? req.query.source : 'all';
|
|
573
|
-
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
|
574
|
-
let results = [];
|
|
575
|
-
if (source === 'clawhub' || source === 'all')
|
|
576
|
-
results = results.concat(await fetchClawHubSkills(q, limit));
|
|
577
|
-
if (source === 'agent-foundry' || source === 'all')
|
|
578
|
-
results = results.concat(await fetchAgentFoundrySkills(q, limit));
|
|
579
|
-
results.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
|
580
|
-
res.json({ ok: true, total: results.length, skills: results.slice(0, limit) });
|
|
581
800
|
});
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
app.post('/api/
|
|
585
|
-
const body = req.body;
|
|
586
|
-
|
|
587
|
-
|
|
801
|
+
/* ========== 阶段一-2:补全 Pro API ========== */
|
|
802
|
+
// 记录用户接受的补全(用于连续推荐)
|
|
803
|
+
app.post('/api/completion/accept', (req, res) => {
|
|
804
|
+
const body = (req.body ?? {});
|
|
805
|
+
const filePath = typeof body?.filePath === 'string' ? body.filePath.trim() : '';
|
|
806
|
+
const cursorOffset = typeof body?.cursorOffset === 'number' ? body.cursorOffset : 0;
|
|
807
|
+
const text = typeof body?.text === 'string' ? body.text : '';
|
|
808
|
+
if (!filePath || !text) {
|
|
809
|
+
res.status(400).json({ ok: false, error: '缺少 filePath 或 text' });
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (!completionEngine) {
|
|
813
|
+
res.json({ ok: false, error: '补全引擎未初始化' });
|
|
588
814
|
return;
|
|
589
815
|
}
|
|
590
816
|
try {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (!saveJsonFile((0, path_1.join)(dir, 'meta.json'), { ...body, installedAt: new Date().toISOString() })) {
|
|
594
|
-
res.status(500).json({ ok: false, error: '技能元数据写入失败' });
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
const installed = loadJsonFile(installedSkillsFile, []);
|
|
598
|
-
if (!installed.find((s) => s.id === body.id)) {
|
|
599
|
-
installed.push(body);
|
|
600
|
-
if (!saveJsonFile(installedSkillsFile, installed)) {
|
|
601
|
-
res.status(500).json({ ok: false, error: '已安装列表写入失败' });
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
res.json({ ok: true, message: `技能「${body.name}」已记录为已安装` });
|
|
817
|
+
completionEngine.recordAcceptance(filePath, cursorOffset, text);
|
|
818
|
+
res.json({ ok: true });
|
|
606
819
|
}
|
|
607
820
|
catch (e) {
|
|
608
|
-
res.status(500).json({ ok: false, error:
|
|
821
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
609
822
|
}
|
|
610
823
|
});
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
});
|
|
614
|
-
const automationsFile = (0, path_1.join)(homeDir, 'automations.json');
|
|
615
|
-
app.get('/api/automations', (_req, res) => {
|
|
616
|
-
res.json({ ok: true, automations: loadJsonFile(automationsFile, []) });
|
|
617
|
-
});
|
|
618
|
-
app.post('/api/automations', async (req, res) => {
|
|
824
|
+
// 推荐下一个改动点(补全 Pro 连续推荐)
|
|
825
|
+
app.post('/api/completion/suggest-next', (req, res) => {
|
|
619
826
|
const body = (req.body ?? {});
|
|
620
|
-
const
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
827
|
+
const filePath = typeof body?.filePath === 'string' ? body.filePath.trim() : '';
|
|
828
|
+
const fileContent = typeof body?.fileContent === 'string' ? body.fileContent : '';
|
|
829
|
+
const cursorOffset = typeof body?.cursorOffset === 'number' ? body.cursorOffset : fileContent.length;
|
|
830
|
+
if (!filePath || !fileContent) {
|
|
831
|
+
res.status(400).json({ ok: false, error: '缺少 filePath 或 fileContent' });
|
|
624
832
|
return;
|
|
625
833
|
}
|
|
626
|
-
|
|
627
|
-
|
|
834
|
+
if (!completionEngine) {
|
|
835
|
+
res.json({ ok: true, suggested: false });
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const suggestion = completionEngine.suggestNext(filePath, fileContent, cursorOffset);
|
|
840
|
+
res.json({ ok: true, ...suggestion });
|
|
841
|
+
}
|
|
842
|
+
catch (e) {
|
|
843
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
/* ========== P2-1: 设计稿转代码 API(多模态,图片→代码) ========== */
|
|
847
|
+
app.post('/api/design-to-code', async (req, res) => {
|
|
848
|
+
const body = (req.body ?? {});
|
|
849
|
+
const image = typeof body?.image === 'string' ? body.image.trim() : '';
|
|
850
|
+
const framework = body?.framework || 'html';
|
|
851
|
+
const instructions = typeof body?.instructions === 'string' ? body.instructions : undefined;
|
|
852
|
+
const previousCode = typeof body?.previousCode === 'string' ? body.previousCode : undefined;
|
|
853
|
+
const feedback = typeof body?.feedback === 'string' ? body.feedback : undefined;
|
|
854
|
+
if (!image) {
|
|
855
|
+
res.status(400).json({ ok: false, error: '缺少 image 字段(base64 或 URL)' });
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (!designToCodeEngine) {
|
|
859
|
+
res.status(503).json({ ok: false, error: '未配置多模态模型。请在模型设置中添加支持 vision 能力的模型(如 GPT-4V、Claude 3、通义千问 VL 等)。' });
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const result = await designToCodeEngine.generate({
|
|
864
|
+
image,
|
|
865
|
+
framework,
|
|
866
|
+
instructions,
|
|
867
|
+
previousCode,
|
|
868
|
+
feedback,
|
|
869
|
+
});
|
|
870
|
+
res.json({ ok: true, ...result });
|
|
871
|
+
}
|
|
872
|
+
catch (e) {
|
|
873
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
/* ========== P2-2: Git 集成 API ========== */
|
|
877
|
+
// 获取 Git 状态
|
|
878
|
+
app.get('/api/git/status', async (_req, res) => {
|
|
879
|
+
try {
|
|
880
|
+
const status = await gitIntegration.status();
|
|
881
|
+
res.json({ ok: true, ...status });
|
|
882
|
+
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
// 获取文件 diff
|
|
888
|
+
app.post('/api/git/diff', async (req, res) => {
|
|
889
|
+
const body = (req.body ?? {});
|
|
890
|
+
const path = typeof body?.path === 'string' ? body.path : undefined;
|
|
891
|
+
const staged = body?.staged === true;
|
|
892
|
+
try {
|
|
893
|
+
const diffs = await gitIntegration.diff(path, staged);
|
|
894
|
+
res.json({ ok: true, diffs });
|
|
895
|
+
}
|
|
896
|
+
catch (e) {
|
|
897
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
// 暂存文件
|
|
901
|
+
app.post('/api/git/add', async (req, res) => {
|
|
902
|
+
const body = (req.body ?? {});
|
|
903
|
+
const paths = Array.isArray(body?.paths) ? body.paths.filter((p) => typeof p === 'string') : [];
|
|
904
|
+
if (!paths.length) {
|
|
905
|
+
res.status(400).json({ ok: false, error: '缺少 paths 字段' });
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const result = await gitIntegration.add(paths);
|
|
910
|
+
res.json({ ok: result.success, message: result.message });
|
|
911
|
+
}
|
|
912
|
+
catch (e) {
|
|
913
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
// 取消暂存
|
|
917
|
+
app.post('/api/git/reset', async (req, res) => {
|
|
918
|
+
const body = (req.body ?? {});
|
|
919
|
+
const paths = Array.isArray(body?.paths) ? body.paths.filter((p) => typeof p === 'string') : [];
|
|
920
|
+
if (!paths.length) {
|
|
921
|
+
res.status(400).json({ ok: false, error: '缺少 paths 字段' });
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
try {
|
|
925
|
+
const result = await gitIntegration.reset(paths);
|
|
926
|
+
res.json({ ok: result.success, message: result.message });
|
|
927
|
+
}
|
|
928
|
+
catch (e) {
|
|
929
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
// 提交
|
|
933
|
+
app.post('/api/git/commit', async (req, res) => {
|
|
934
|
+
const body = (req.body ?? {});
|
|
935
|
+
const message = typeof body?.message === 'string' ? body.message.trim() : '';
|
|
936
|
+
const paths = Array.isArray(body?.paths) ? body.paths.filter((p) => typeof p === 'string') : undefined;
|
|
937
|
+
if (!message) {
|
|
938
|
+
res.status(400).json({ ok: false, error: '提交信息不能为空' });
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
const result = await gitIntegration.commit(message, paths);
|
|
943
|
+
res.json({ ok: result.success, hash: result.hash, message: result.message });
|
|
944
|
+
}
|
|
945
|
+
catch (e) {
|
|
946
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
// 获取分支列表
|
|
950
|
+
app.get('/api/git/branches', async (_req, res) => {
|
|
951
|
+
try {
|
|
952
|
+
const branches = await gitIntegration.branches();
|
|
953
|
+
res.json({ ok: true, branches });
|
|
954
|
+
}
|
|
955
|
+
catch (e) {
|
|
956
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
// 切换分支
|
|
960
|
+
app.post('/api/git/checkout', async (req, res) => {
|
|
961
|
+
const body = (req.body ?? {});
|
|
962
|
+
const branch = typeof body?.branch === 'string' ? body.branch.trim() : '';
|
|
963
|
+
const createNew = body?.createNew === true;
|
|
964
|
+
if (!branch) {
|
|
965
|
+
res.status(400).json({ ok: false, error: '缺少 branch 字段' });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const result = await gitIntegration.checkout(branch, createNew);
|
|
970
|
+
res.json({ ok: result.success, message: result.message });
|
|
971
|
+
}
|
|
972
|
+
catch (e) {
|
|
973
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
// 获取提交历史
|
|
977
|
+
app.get('/api/git/log', async (req, res) => {
|
|
978
|
+
const count = typeof req.query?.count === 'string' ? parseInt(req.query.count) : 20;
|
|
979
|
+
try {
|
|
980
|
+
const commits = await gitIntegration.log(Math.min(Math.max(count, 1), 100));
|
|
981
|
+
res.json({ ok: true, commits });
|
|
982
|
+
}
|
|
983
|
+
catch (e) {
|
|
984
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
// 推送
|
|
988
|
+
app.post('/api/git/push', async (req, res) => {
|
|
989
|
+
const body = (req.body ?? {});
|
|
990
|
+
const remote = typeof body?.remote === 'string' ? body.remote : 'origin';
|
|
991
|
+
const branch = typeof body?.branch === 'string' ? body.branch : undefined;
|
|
992
|
+
const force = body?.force === true;
|
|
993
|
+
try {
|
|
994
|
+
const result = await gitIntegration.push(remote, branch, force);
|
|
995
|
+
res.json({ ok: result.success, message: result.message });
|
|
996
|
+
}
|
|
997
|
+
catch (e) {
|
|
998
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
// 拉取
|
|
1002
|
+
app.post('/api/git/pull', async (req, res) => {
|
|
1003
|
+
const body = (req.body ?? {});
|
|
1004
|
+
const remote = typeof body?.remote === 'string' ? body.remote : 'origin';
|
|
1005
|
+
const branch = typeof body?.branch === 'string' ? body.branch : undefined;
|
|
1006
|
+
try {
|
|
1007
|
+
const result = await gitIntegration.pull(remote, branch);
|
|
1008
|
+
res.json({ ok: result.success, message: result.message });
|
|
1009
|
+
}
|
|
1010
|
+
catch (e) {
|
|
1011
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
// 初始化仓库
|
|
1015
|
+
app.post('/api/git/init', async (_req, res) => {
|
|
1016
|
+
try {
|
|
1017
|
+
const result = await gitIntegration.init();
|
|
1018
|
+
res.json({ ok: result.success, message: result.message });
|
|
1019
|
+
}
|
|
1020
|
+
catch (e) {
|
|
1021
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
/* ========== P2-3: 团队协作 API ========== */
|
|
1025
|
+
app.get('/api/team', (_req, res) => {
|
|
1026
|
+
try {
|
|
1027
|
+
const team = teamManager.getTeam();
|
|
1028
|
+
const stats = teamManager.getStats();
|
|
1029
|
+
res.json({ ok: true, team, stats });
|
|
1030
|
+
}
|
|
1031
|
+
catch (e) {
|
|
1032
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
app.put('/api/team/config', (req, res) => {
|
|
1036
|
+
try {
|
|
1037
|
+
const config = teamManager.updateConfig(req.body || {});
|
|
1038
|
+
res.json({ ok: true, config });
|
|
1039
|
+
}
|
|
1040
|
+
catch (e) {
|
|
1041
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
app.get('/api/team/members', (_req, res) => {
|
|
1045
|
+
try {
|
|
1046
|
+
res.json({ ok: true, members: teamManager.getMembers() });
|
|
1047
|
+
}
|
|
1048
|
+
catch (e) {
|
|
1049
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
app.post('/api/team/members/invite', (req, res) => {
|
|
1053
|
+
const body = (req.body ?? {});
|
|
1054
|
+
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
1055
|
+
const email = typeof body?.email === 'string' ? body.email.trim() : '';
|
|
1056
|
+
const role = body?.role || 'developer';
|
|
1057
|
+
if (!name || !email) {
|
|
1058
|
+
res.status(400).json({ ok: false, error: '缺少 name 或 email' });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
const member = teamManager.inviteMember(name, email, role);
|
|
1063
|
+
res.json({ ok: true, member });
|
|
1064
|
+
}
|
|
1065
|
+
catch (e) {
|
|
1066
|
+
res.status(400).json({ ok: false, error: e.message });
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
app.put('/api/team/members/:id/role', (req, res) => {
|
|
1070
|
+
try {
|
|
1071
|
+
const role = (req.body ?? {}).role;
|
|
1072
|
+
const member = teamManager.updateMemberRole(req.params.id, role);
|
|
1073
|
+
res.json({ ok: true, member });
|
|
1074
|
+
}
|
|
1075
|
+
catch (e) {
|
|
1076
|
+
res.status(400).json({ ok: false, error: e.message });
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
app.delete('/api/team/members/:id', (req, res) => {
|
|
1080
|
+
try {
|
|
1081
|
+
teamManager.removeMember(req.params.id);
|
|
1082
|
+
res.json({ ok: true });
|
|
1083
|
+
}
|
|
1084
|
+
catch (e) {
|
|
1085
|
+
res.status(400).json({ ok: false, error: e.message });
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
app.get('/api/team/tasks', (req, res) => {
|
|
1089
|
+
try {
|
|
1090
|
+
const status = typeof req.query?.status === 'string' ? req.query.status : undefined;
|
|
1091
|
+
res.json({ ok: true, tasks: teamManager.getTasks(status) });
|
|
1092
|
+
}
|
|
1093
|
+
catch (e) {
|
|
1094
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
app.post('/api/team/tasks', (req, res) => {
|
|
1098
|
+
const body = (req.body ?? {});
|
|
1099
|
+
const title = typeof body?.title === 'string' ? body.title.trim() : '';
|
|
1100
|
+
if (!title) {
|
|
1101
|
+
res.status(400).json({ ok: false, error: '缺少 title' });
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
const task = teamManager.createTask(title, body.createdBy || 'system', body);
|
|
1106
|
+
res.json({ ok: true, task });
|
|
1107
|
+
}
|
|
1108
|
+
catch (e) {
|
|
1109
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
app.put('/api/team/tasks/:id', (req, res) => {
|
|
1113
|
+
try {
|
|
1114
|
+
res.json({ ok: true, task: teamManager.updateTask(req.params.id, req.body || {}) });
|
|
1115
|
+
}
|
|
1116
|
+
catch (e) {
|
|
1117
|
+
res.status(400).json({ ok: false, error: e.message });
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
app.delete('/api/team/tasks/:id', (req, res) => {
|
|
1121
|
+
try {
|
|
1122
|
+
teamManager.deleteTask(req.params.id);
|
|
1123
|
+
res.json({ ok: true });
|
|
1124
|
+
}
|
|
1125
|
+
catch (e) {
|
|
1126
|
+
res.status(400).json({ ok: false, error: e.message });
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
app.post('/api/team/tasks/:id/comments', (req, res) => {
|
|
1130
|
+
const body = (req.body ?? {});
|
|
1131
|
+
const content = typeof body?.content === 'string' ? body.content.trim() : '';
|
|
1132
|
+
if (!content) {
|
|
1133
|
+
res.status(400).json({ ok: false, error: '缺少 content' });
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
const task = teamManager.addComment(req.params.id, body.author || 'anonymous', content);
|
|
1138
|
+
res.json({ ok: true, task });
|
|
1139
|
+
}
|
|
1140
|
+
catch (e) {
|
|
1141
|
+
res.status(400).json({ ok: false, error: e.message });
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
app.get('/api/team/stats', (_req, res) => {
|
|
1145
|
+
try {
|
|
1146
|
+
res.json({ ok: true, stats: teamManager.getStats() });
|
|
1147
|
+
}
|
|
1148
|
+
catch (e) {
|
|
1149
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
/* ========== P3-2: SOLO 全自主编程 API ========== */
|
|
1153
|
+
// 启动 SOLO 任务
|
|
1154
|
+
app.post('/api/solo/run', async (req, res) => {
|
|
1155
|
+
const body = (req.body ?? {});
|
|
1156
|
+
const goal = typeof body?.goal === 'string' ? body.goal.trim() : '';
|
|
1157
|
+
if (!goal) {
|
|
1158
|
+
res.status(400).json({ ok: false, error: '缺少 goal' });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const taskId = `solo-${Date.now()}`;
|
|
1162
|
+
const config = {
|
|
1163
|
+
cwd: serverWorkspaceDir,
|
|
1164
|
+
goal,
|
|
1165
|
+
maxTasks: body.maxTasks || 8,
|
|
1166
|
+
maxRetries: body.maxRetries || 3,
|
|
1167
|
+
parallel: body.parallel || false,
|
|
1168
|
+
maxParallel: body.maxParallel || 2,
|
|
1169
|
+
planOnly: body.planOnly || false,
|
|
1170
|
+
verifyOnly: body.verifyOnly || false,
|
|
1171
|
+
autoCorrect: body.autoCorrect !== false,
|
|
1172
|
+
};
|
|
1173
|
+
// 创建子任务执行器(简化版,实际应委托给 orchestrator)
|
|
1174
|
+
const executor = async (focusedGoal) => {
|
|
1175
|
+
try {
|
|
1176
|
+
// 简化执行:返回成功,实际应调用 orchestrator
|
|
1177
|
+
return { ok: true, output: `已执行: ${focusedGoal}`, touchedFiles: [], iterations: 1 };
|
|
1178
|
+
}
|
|
1179
|
+
catch (e) {
|
|
1180
|
+
return { ok: false, output: e.message, touchedFiles: [], iterations: 0 };
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
const agent = new solo_agent_1.SoloAgent(config, executor);
|
|
1184
|
+
soloTasks.set(taskId, { agent, status: 'running' });
|
|
1185
|
+
// 异步执行
|
|
1186
|
+
(async () => {
|
|
1187
|
+
try {
|
|
1188
|
+
const report = await agent.run();
|
|
1189
|
+
const task = soloTasks.get(taskId);
|
|
1190
|
+
if (task) {
|
|
1191
|
+
task.report = report;
|
|
1192
|
+
task.status = report.overall === 'failed' ? 'failed' : 'completed';
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
catch (e) {
|
|
1196
|
+
const task = soloTasks.get(taskId);
|
|
1197
|
+
if (task) {
|
|
1198
|
+
task.status = 'failed';
|
|
1199
|
+
task.report = {
|
|
1200
|
+
goal, cwd: serverWorkspaceDir, overall: 'failed', phase: 'failed',
|
|
1201
|
+
startedAt: new Date().toISOString(), completedAt: new Date().toISOString(),
|
|
1202
|
+
totalTasks: 0, completedTasks: 0, failedTasks: 0, skippedTasks: 0,
|
|
1203
|
+
tasks: [], summary: `执行失败: ${e.message}`, events: [],
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
})();
|
|
1208
|
+
res.json({ ok: true, taskId, message: 'SOLO 任务已启动' });
|
|
1209
|
+
});
|
|
1210
|
+
// 获取 SOLO 任务状态
|
|
1211
|
+
app.get('/api/solo/status/:id', (req, res) => {
|
|
1212
|
+
const task = soloTasks.get(req.params.id);
|
|
1213
|
+
if (!task) {
|
|
1214
|
+
res.status(404).json({ ok: false, error: '任务不存在' });
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const status = task.agent.getStatus();
|
|
1218
|
+
res.json({ ok: true, taskId: req.params.id, status: task.status, ...status });
|
|
1219
|
+
});
|
|
1220
|
+
// 获取 SOLO 任务报告
|
|
1221
|
+
app.get('/api/solo/report/:id', (req, res) => {
|
|
1222
|
+
const task = soloTasks.get(req.params.id);
|
|
1223
|
+
if (!task) {
|
|
1224
|
+
res.status(404).json({ ok: false, error: '任务不存在' });
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (!task.report) {
|
|
1228
|
+
res.status(202).json({ ok: false, error: '任务尚未完成', status: task.status });
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
res.json({ ok: true, report: task.report });
|
|
1232
|
+
});
|
|
1233
|
+
// 取消 SOLO 任务
|
|
1234
|
+
app.post('/api/solo/cancel/:id', (req, res) => {
|
|
1235
|
+
const task = soloTasks.get(req.params.id);
|
|
1236
|
+
if (!task) {
|
|
1237
|
+
res.status(404).json({ ok: false, error: '任务不存在' });
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (task.status === 'running') {
|
|
1241
|
+
task.status = 'failed';
|
|
1242
|
+
res.json({ ok: true, message: '任务已取消' });
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
res.json({ ok: false, error: '任务已结束,无法取消' });
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
// 列出所有 SOLO 任务
|
|
1249
|
+
app.get('/api/solo/list', (_req, res) => {
|
|
1250
|
+
const tasks = Array.from(soloTasks.entries()).map(([id, task]) => ({
|
|
1251
|
+
id,
|
|
1252
|
+
status: task.status,
|
|
1253
|
+
goal: task.report?.goal || '',
|
|
1254
|
+
startedAt: task.report?.startedAt,
|
|
1255
|
+
completedAt: task.report?.completedAt,
|
|
1256
|
+
}));
|
|
1257
|
+
res.json({ ok: true, tasks });
|
|
1258
|
+
});
|
|
1259
|
+
/* ========== 阶段二-1:多智能体协同 API ========== */
|
|
1260
|
+
// 运行多智能体协同(架构师/开发/测试/评审)
|
|
1261
|
+
app.post('/api/multi-agent/run', async (req, res) => {
|
|
1262
|
+
const body = (req.body ?? {});
|
|
1263
|
+
const goal = typeof body?.goal === 'string' ? body.goal.trim() : '';
|
|
1264
|
+
if (!goal) {
|
|
1265
|
+
res.status(400).json({ ok: false, error: '缺少 goal' });
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (!sharedModelRouter) {
|
|
1269
|
+
res.status(500).json({ ok: false, error: '模型未配置' });
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const config = {
|
|
1273
|
+
roles: body.roles || ['architect', 'developer', 'tester', 'reviewer'],
|
|
1274
|
+
maxRounds: body.maxRounds || 3,
|
|
1275
|
+
enableReviewLoop: body.enableReviewLoop !== false,
|
|
1276
|
+
};
|
|
1277
|
+
try {
|
|
1278
|
+
const result = await (0, multi_agent_1.runMultiAgent)(sharedModelRouter, goal, config, body.context);
|
|
1279
|
+
res.json({ ok: true, ...result });
|
|
1280
|
+
}
|
|
1281
|
+
catch (e) {
|
|
1282
|
+
res.status(500).json({ ok: false, error: '多智能体协同失败: ' + e.message });
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
// 获取角色配置
|
|
1286
|
+
app.get('/api/multi-agent/roles', (_req, res) => {
|
|
1287
|
+
try {
|
|
1288
|
+
Promise.resolve().then(() => __importStar(require('../agent/multi-agent'))).then(({ AGENT_ROLES }) => {
|
|
1289
|
+
const roles = Object.values(AGENT_ROLES).map((r) => ({
|
|
1290
|
+
role: r.role,
|
|
1291
|
+
name: r.name,
|
|
1292
|
+
description: r.description,
|
|
1293
|
+
capabilities: r.capabilities,
|
|
1294
|
+
}));
|
|
1295
|
+
res.json({ ok: true, roles });
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
catch (e) {
|
|
1299
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
/* ========== 阶段二-2:事件驱动 Agent API ========== */
|
|
1303
|
+
// 获取事件驱动配置
|
|
1304
|
+
app.get('/api/event-driven/config', (_req, res) => {
|
|
1305
|
+
try {
|
|
1306
|
+
res.json({ ok: true, config: eventDrivenManager.getConfig() });
|
|
1307
|
+
}
|
|
1308
|
+
catch (e) {
|
|
1309
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
// 获取事件历史
|
|
1313
|
+
app.get('/api/event-driven/events', (req, res) => {
|
|
1314
|
+
try {
|
|
1315
|
+
const limit = typeof req.query?.limit === 'string' ? parseInt(req.query.limit, 10) : 50;
|
|
1316
|
+
res.json({ ok: true, events: eventDrivenManager.getEvents(limit) });
|
|
1317
|
+
}
|
|
1318
|
+
catch (e) {
|
|
1319
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
// 手动触发事件
|
|
1323
|
+
app.post('/api/event-driven/trigger', (req, res) => {
|
|
1324
|
+
const body = (req.body ?? {});
|
|
1325
|
+
if (!body.type || !body.title) {
|
|
1326
|
+
res.status(400).json({ ok: false, error: '缺少 type 或 title' });
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
eventDrivenManager.triggerEvent({
|
|
1330
|
+
type: body.type,
|
|
1331
|
+
severity: body.severity || 'info',
|
|
1332
|
+
title: body.title,
|
|
1333
|
+
description: body.description || '',
|
|
1334
|
+
source: body.source || 'manual',
|
|
1335
|
+
payload: body.payload,
|
|
1336
|
+
}).then((event) => res.json({ ok: true, event }))
|
|
1337
|
+
.catch((e) => res.status(500).json({ ok: false, error: e.message }));
|
|
1338
|
+
});
|
|
1339
|
+
// Cron 任务 CRUD
|
|
1340
|
+
app.post('/api/event-driven/cron', (req, res) => {
|
|
1341
|
+
const body = (req.body ?? {});
|
|
1342
|
+
if (!body.name || !body.cron || !body.task) {
|
|
1343
|
+
res.status(400).json({ ok: false, error: '缺少 name/cron/task' });
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
try {
|
|
1347
|
+
const task = eventDrivenManager.addCronTask({
|
|
1348
|
+
name: body.name,
|
|
1349
|
+
cron: body.cron,
|
|
1350
|
+
task: body.task,
|
|
1351
|
+
enabled: body.enabled !== false,
|
|
1352
|
+
debounceMs: body.debounceMs || 0,
|
|
1353
|
+
});
|
|
1354
|
+
res.json({ ok: true, task });
|
|
1355
|
+
}
|
|
1356
|
+
catch (e) {
|
|
1357
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
app.put('/api/event-driven/cron/:id', (req, res) => {
|
|
1361
|
+
try {
|
|
1362
|
+
const task = eventDrivenManager.updateCronTask(req.params.id, (req.body ?? {}));
|
|
1363
|
+
if (!task) {
|
|
1364
|
+
res.status(404).json({ ok: false, error: '任务不存在' });
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
res.json({ ok: true, task });
|
|
1368
|
+
}
|
|
1369
|
+
catch (e) {
|
|
1370
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
app.delete('/api/event-driven/cron/:id', (req, res) => {
|
|
1374
|
+
try {
|
|
1375
|
+
const ok = eventDrivenManager.removeCronTask(req.params.id);
|
|
1376
|
+
res.json({ ok, message: ok ? '已删除' : '任务不存在' });
|
|
1377
|
+
}
|
|
1378
|
+
catch (e) {
|
|
1379
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
// Webhook 接收
|
|
1383
|
+
app.post('/api/event-driven/webhook/:path', (req, res) => {
|
|
1384
|
+
const path = `/webhook/${req.params.path}`;
|
|
1385
|
+
eventDrivenManager.handleWebhook(path, (req.body || {}), req.headers)
|
|
1386
|
+
.then((result) => res.json(result))
|
|
1387
|
+
.catch((e) => res.status(500).json({ ok: false, error: e.message }));
|
|
1388
|
+
});
|
|
1389
|
+
/* ========== 阶段二-3:自定义 Agent API ========== */
|
|
1390
|
+
// 获取所有自定义 Agent
|
|
1391
|
+
app.get('/api/custom-agents', (req, res) => {
|
|
1392
|
+
try {
|
|
1393
|
+
const category = typeof req.query?.category === 'string' ? req.query.category : undefined;
|
|
1394
|
+
res.json({ ok: true, agents: customAgentManager.getAllAgents(category) });
|
|
1395
|
+
}
|
|
1396
|
+
catch (e) {
|
|
1397
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
// 获取 Agent 分类
|
|
1401
|
+
app.get('/api/custom-agents/categories', (_req, res) => {
|
|
1402
|
+
try {
|
|
1403
|
+
res.json({ ok: true, categories: customAgentManager.getCategories() });
|
|
1404
|
+
}
|
|
1405
|
+
catch (e) {
|
|
1406
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
// 匹配推荐 Agent
|
|
1410
|
+
app.post('/api/custom-agents/match', (req, res) => {
|
|
1411
|
+
const body = (req.body ?? {});
|
|
1412
|
+
const input = typeof body?.input === 'string' ? body.input : '';
|
|
1413
|
+
if (!input) {
|
|
1414
|
+
res.status(400).json({ ok: false, error: '缺少 input' });
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
const limit = typeof body?.limit === 'number' ? body.limit : 3;
|
|
1419
|
+
res.json({ ok: true, agents: customAgentManager.matchAgents(input, limit) });
|
|
1420
|
+
}
|
|
1421
|
+
catch (e) {
|
|
1422
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
// 获取单个 Agent
|
|
1426
|
+
app.get('/api/custom-agents/:id', (req, res) => {
|
|
1427
|
+
try {
|
|
1428
|
+
const agent = customAgentManager.getAgent(req.params.id);
|
|
1429
|
+
if (!agent) {
|
|
1430
|
+
res.status(404).json({ ok: false, error: 'Agent 不存在' });
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
res.json({ ok: true, agent });
|
|
1434
|
+
}
|
|
1435
|
+
catch (e) {
|
|
1436
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
// 创建自定义 Agent
|
|
1440
|
+
app.post('/api/custom-agents', (req, res) => {
|
|
1441
|
+
const body = (req.body ?? {});
|
|
1442
|
+
if (!body.name || !body.systemPrompt) {
|
|
1443
|
+
res.status(400).json({ ok: false, error: '缺少 name 或 systemPrompt' });
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
const agent = customAgentManager.createAgent({
|
|
1448
|
+
name: body.name,
|
|
1449
|
+
description: body.description || '',
|
|
1450
|
+
systemPrompt: body.systemPrompt,
|
|
1451
|
+
tools: body.tools || [],
|
|
1452
|
+
modelConfig: body.modelConfig || { temperature: 0.3, maxTokens: 2000, timeoutMs: 30000 },
|
|
1453
|
+
triggers: body.triggers || [],
|
|
1454
|
+
icon: body.icon || '🤖',
|
|
1455
|
+
category: body.category || 'custom',
|
|
1456
|
+
enabled: body.enabled !== false,
|
|
1457
|
+
author: body.author,
|
|
1458
|
+
version: body.version || '1.0.0',
|
|
1459
|
+
});
|
|
1460
|
+
res.json({ ok: true, agent });
|
|
1461
|
+
}
|
|
1462
|
+
catch (e) {
|
|
1463
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
// 更新自定义 Agent
|
|
1467
|
+
app.put('/api/custom-agents/:id', (req, res) => {
|
|
1468
|
+
try {
|
|
1469
|
+
const agent = customAgentManager.updateAgent(req.params.id, (req.body ?? {}));
|
|
1470
|
+
if (!agent) {
|
|
1471
|
+
res.status(404).json({ ok: false, error: 'Agent 不存在' });
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
res.json({ ok: true, agent });
|
|
1475
|
+
}
|
|
1476
|
+
catch (e) {
|
|
1477
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
// 删除自定义 Agent
|
|
1481
|
+
app.delete('/api/custom-agents/:id', (req, res) => {
|
|
1482
|
+
try {
|
|
1483
|
+
const ok = customAgentManager.deleteAgent(req.params.id);
|
|
1484
|
+
res.json({ ok, message: ok ? '已删除' : 'Agent 不存在或为内置 Agent' });
|
|
1485
|
+
}
|
|
1486
|
+
catch (e) {
|
|
1487
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
// 执行自定义 Agent
|
|
1491
|
+
app.post('/api/custom-agents/:id/execute', async (req, res) => {
|
|
1492
|
+
const body = (req.body ?? {});
|
|
1493
|
+
const input = typeof body?.input === 'string' ? body.input : '';
|
|
1494
|
+
if (!input) {
|
|
1495
|
+
res.status(400).json({ ok: false, error: '缺少 input' });
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (!sharedModelRouter) {
|
|
1499
|
+
res.status(500).json({ ok: false, error: '模型未配置' });
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
try {
|
|
1503
|
+
const result = await customAgentManager.executeAgent(req.params.id, input, sharedModelRouter, body?.context);
|
|
1504
|
+
res.json({ ok: result.success, ...result });
|
|
1505
|
+
}
|
|
1506
|
+
catch (e) {
|
|
1507
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
/* ========== 打开本地文件夹/浏览器 ========== */
|
|
1511
|
+
app.post('/api/open/folder', (req, res) => {
|
|
1512
|
+
const body = (req.body ?? {});
|
|
1513
|
+
const dir = typeof body?.path === 'string' ? body.path.trim() : serverWorkspaceDir;
|
|
1514
|
+
if (!dir || !assertPathAllowed(dir, res))
|
|
1515
|
+
return;
|
|
1516
|
+
try {
|
|
1517
|
+
openFolder(dir);
|
|
1518
|
+
res.json({ ok: true });
|
|
1519
|
+
}
|
|
1520
|
+
catch (e) {
|
|
1521
|
+
res.status(500).json({ ok: false, error: '打开失败: ' + e.message });
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
app.post('/api/open/browser', (req, res) => {
|
|
1525
|
+
const body = (req.body ?? {});
|
|
1526
|
+
const url = typeof body?.url === 'string' ? body.url.trim() : '';
|
|
1527
|
+
if (!url) {
|
|
1528
|
+
res.status(400).json({ ok: false, error: '缺少 url 字段' });
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
try {
|
|
1532
|
+
openBrowser(url);
|
|
1533
|
+
res.json({ ok: true });
|
|
1534
|
+
}
|
|
1535
|
+
catch (e) {
|
|
1536
|
+
res.status(500).json({ ok: false, error: '打开失败: ' + e.message });
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
/* ========== 上传文件/图片 ========== */
|
|
1540
|
+
const uploadsDir = () => (0, path_1.join)(homeDir, 'uploads');
|
|
1541
|
+
app.post('/api/upload', (req, res) => {
|
|
1542
|
+
const body = (req.body ?? {});
|
|
1543
|
+
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
1544
|
+
const mime = typeof body?.mime === 'string' ? body.mime.trim() : 'application/octet-stream';
|
|
1545
|
+
const data = typeof body?.dataBase64 === 'string' ? body.dataBase64.trim() : '';
|
|
1546
|
+
if (!name || !data) {
|
|
1547
|
+
res.status(400).json({ ok: false, error: '缺少 name 或 dataBase64 字段' });
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
try {
|
|
1551
|
+
(0, fs_1.mkdirSync)(uploadsDir(), { recursive: true });
|
|
1552
|
+
const safeName = name.replace(/[^a-zA-Z0-9_.\-]/g, '_');
|
|
1553
|
+
const dest = (0, path_1.join)(uploadsDir(), `${Date.now()}_${safeName}`);
|
|
1554
|
+
(0, fs_1.writeFileSync)(dest, Buffer.from(data, 'base64'));
|
|
1555
|
+
res.json({ ok: true, path: dest, name, mime });
|
|
1556
|
+
}
|
|
1557
|
+
catch (e) {
|
|
1558
|
+
res.status(500).json({ ok: false, error: '上传失败: ' + e.message });
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
/* ========== 系统截图(调用 Windows 截图工具,不弹浏览器分享框) ========== */
|
|
1562
|
+
app.post('/api/screenshot', (_req, res) => {
|
|
1563
|
+
try {
|
|
1564
|
+
// Windows 10/11 内置截图工具(和 Win+Shift+S 效果一样)
|
|
1565
|
+
// 调用后直接进入截图模式,用户截图后图片保存到剪贴板
|
|
1566
|
+
if (process.platform === 'win32') {
|
|
1567
|
+
(0, child_process_1.exec)('explorer.exe ms-screenclip:', (err) => {
|
|
1568
|
+
if (err) {
|
|
1569
|
+
res.status(500).json({ ok: false, error: '启动截图工具失败: ' + err.message });
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
res.json({ ok: true, message: '截图工具已启动,截图后按 Ctrl+V 粘贴到输入框' });
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
res.status(400).json({ ok: false, error: '仅支持 Windows 系统' });
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
catch (e) {
|
|
1581
|
+
res.status(500).json({ ok: false, error: '启动截图工具失败: ' + e.message });
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
/* ========== 电脑操作(鼠标/键盘/截图,用语言控制电脑) ========== */
|
|
1585
|
+
// 执行 PowerShell 命令的辅助函数
|
|
1586
|
+
const runPowerShell = (script) => {
|
|
1587
|
+
return new Promise((resolve, reject) => {
|
|
1588
|
+
const ps = (0, child_process_1.spawn)('powershell.exe', ['-NoProfile', '-Command', script], {
|
|
1589
|
+
windowsHide: true,
|
|
1590
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1591
|
+
});
|
|
1592
|
+
let stdout = '';
|
|
1593
|
+
let stderr = '';
|
|
1594
|
+
ps.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
1595
|
+
ps.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
1596
|
+
ps.on('close', (code) => {
|
|
1597
|
+
if (code === 0)
|
|
1598
|
+
resolve(stdout.trim());
|
|
1599
|
+
else
|
|
1600
|
+
reject(new Error(stderr || `PowerShell exited with code ${code}`));
|
|
1601
|
+
});
|
|
1602
|
+
ps.on('error', reject);
|
|
1603
|
+
});
|
|
1604
|
+
};
|
|
1605
|
+
// 截图
|
|
1606
|
+
app.post('/api/computer/screenshot', async (_req, res) => {
|
|
1607
|
+
try {
|
|
1608
|
+
if (process.platform !== 'win32') {
|
|
1609
|
+
res.status(400).json({ ok: false, error: '仅支持 Windows 系统' });
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const script = `
|
|
1613
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
1614
|
+
Add-Type -AssemblyName System.Drawing
|
|
1615
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
1616
|
+
$bounds = $screen.Bounds
|
|
1617
|
+
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
|
|
1618
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
1619
|
+
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
1620
|
+
$ms = New-Object System.IO.MemoryStream
|
|
1621
|
+
$bitmap.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
1622
|
+
$bytes = $ms.ToArray()
|
|
1623
|
+
[Convert]::ToBase64String($bytes)
|
|
1624
|
+
`;
|
|
1625
|
+
const base64 = await runPowerShell(script);
|
|
1626
|
+
res.json({ ok: true, image: 'data:image/png;base64,' + base64, width: 1920, height: 1080 });
|
|
1627
|
+
}
|
|
1628
|
+
catch (e) {
|
|
1629
|
+
res.status(500).json({ ok: false, error: '截图失败: ' + e.message });
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
// 移动鼠标
|
|
1633
|
+
app.post('/api/computer/mouse/move', async (req, res) => {
|
|
1634
|
+
try {
|
|
1635
|
+
const body = (req.body ?? {});
|
|
1636
|
+
const x = parseInt(body?.x ?? '0');
|
|
1637
|
+
const y = parseInt(body?.y ?? '0');
|
|
1638
|
+
if (isNaN(x) || isNaN(y)) {
|
|
1639
|
+
res.status(400).json({ ok: false, error: '缺少 x 或 y 坐标' });
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
const script = `
|
|
1643
|
+
Add-Type @"
|
|
1644
|
+
using System;
|
|
1645
|
+
using System.Runtime.InteropServices;
|
|
1646
|
+
public class MouseHelper {
|
|
1647
|
+
[DllImport("user32.dll")]
|
|
1648
|
+
public static extern bool SetCursorPos(int X, int Y);
|
|
1649
|
+
}
|
|
1650
|
+
"@
|
|
1651
|
+
[MouseHelper]::SetCursorPos(${x}, ${y}) | Out-Null
|
|
1652
|
+
Write-Output "ok"
|
|
1653
|
+
`;
|
|
1654
|
+
await runPowerShell(script);
|
|
1655
|
+
res.json({ ok: true, x, y });
|
|
1656
|
+
}
|
|
1657
|
+
catch (e) {
|
|
1658
|
+
res.status(500).json({ ok: false, error: '移动鼠标失败: ' + e.message });
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
// 点击鼠标
|
|
1662
|
+
app.post('/api/computer/mouse/click', async (req, res) => {
|
|
1663
|
+
try {
|
|
1664
|
+
const body = (req.body ?? {});
|
|
1665
|
+
const button = (body?.button ?? 'left');
|
|
1666
|
+
const x = body?.x !== undefined ? parseInt(body.x) : null;
|
|
1667
|
+
const y = body?.y !== undefined ? parseInt(body.y) : null;
|
|
1668
|
+
const doubleClick = body?.double === true;
|
|
1669
|
+
let clickFlag = '0x0002'; // left down
|
|
1670
|
+
let upFlag = '0x0004'; // left up
|
|
1671
|
+
if (button === 'right') {
|
|
1672
|
+
clickFlag = '0x0008';
|
|
1673
|
+
upFlag = '0x0010';
|
|
1674
|
+
}
|
|
1675
|
+
const movePart = (x !== null && y !== null) ? `[MouseHelper]::SetCursorPos(${x}, ${y}) | Out-Null; Start-Sleep -Milliseconds 100;` : '';
|
|
1676
|
+
const clickPart = doubleClick
|
|
1677
|
+
? `[MouseHelper]::mouse_event(${clickFlag}, 0, 0, 0, 0); [MouseHelper]::mouse_event(${upFlag}, 0, 0, 0, 0); Start-Sleep -Milliseconds 100; [MouseHelper]::mouse_event(${clickFlag}, 0, 0, 0, 0); [MouseHelper]::mouse_event(${upFlag}, 0, 0, 0, 0);`
|
|
1678
|
+
: `[MouseHelper]::mouse_event(${clickFlag}, 0, 0, 0, 0); [MouseHelper]::mouse_event(${upFlag}, 0, 0, 0, 0);`;
|
|
1679
|
+
const script = `
|
|
1680
|
+
Add-Type @"
|
|
1681
|
+
using System;
|
|
1682
|
+
using System.Runtime.InteropServices;
|
|
1683
|
+
public class MouseHelper {
|
|
1684
|
+
[DllImport("user32.dll")]
|
|
1685
|
+
public static extern bool SetCursorPos(int X, int Y);
|
|
1686
|
+
[DllImport("user32.dll")]
|
|
1687
|
+
public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
|
|
1688
|
+
}
|
|
1689
|
+
"@
|
|
1690
|
+
${movePart}
|
|
1691
|
+
${clickPart}
|
|
1692
|
+
Write-Output "ok"
|
|
1693
|
+
`;
|
|
1694
|
+
await runPowerShell(script);
|
|
1695
|
+
res.json({ ok: true, button, x, y, double: doubleClick });
|
|
1696
|
+
}
|
|
1697
|
+
catch (e) {
|
|
1698
|
+
res.status(500).json({ ok: false, error: '点击鼠标失败: ' + e.message });
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
// 输入文字
|
|
1702
|
+
app.post('/api/computer/keyboard/type', async (req, res) => {
|
|
1703
|
+
try {
|
|
1704
|
+
const body = (req.body ?? {});
|
|
1705
|
+
const text = (body?.text ?? '');
|
|
1706
|
+
if (!text) {
|
|
1707
|
+
res.status(400).json({ ok: false, error: '缺少 text 字段' });
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
// SendKeys 需要转义特殊字符
|
|
1711
|
+
const escaped = text.replace(/([+^%~(){}])/g, '{$1}');
|
|
1712
|
+
const script = `
|
|
1713
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
1714
|
+
[System.Windows.Forms.SendKeys]::SendWait('${escaped.replace(/'/g, "''")}')
|
|
1715
|
+
Write-Output "ok"
|
|
1716
|
+
`;
|
|
1717
|
+
await runPowerShell(script);
|
|
1718
|
+
res.json({ ok: true, text });
|
|
1719
|
+
}
|
|
1720
|
+
catch (e) {
|
|
1721
|
+
res.status(500).json({ ok: false, error: '输入文字失败: ' + e.message });
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
// 按键(快捷键)
|
|
1725
|
+
app.post('/api/computer/keyboard/press', async (req, res) => {
|
|
1726
|
+
try {
|
|
1727
|
+
const body = (req.body ?? {});
|
|
1728
|
+
const key = (body?.key ?? '');
|
|
1729
|
+
if (!key) {
|
|
1730
|
+
res.status(400).json({ ok: false, error: '缺少 key 字段' });
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const script = `
|
|
1734
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
1735
|
+
[System.Windows.Forms.SendKeys]::SendWait('${key.replace(/'/g, "''")}')
|
|
1736
|
+
Write-Output "ok"
|
|
1737
|
+
`;
|
|
1738
|
+
await runPowerShell(script);
|
|
1739
|
+
res.json({ ok: true, key });
|
|
1740
|
+
}
|
|
1741
|
+
catch (e) {
|
|
1742
|
+
res.status(500).json({ ok: false, error: '按键失败: ' + e.message });
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
const nodesFile = (0, path_1.join)(homeDir, 'nodes.json');
|
|
1746
|
+
const sourcesFile = (0, path_1.join)(homeDir, 'sources.json');
|
|
1747
|
+
const nodeDataDir = (0, path_1.join)(homeDir, 'node-data');
|
|
1748
|
+
function loadNodes() {
|
|
1749
|
+
const list = loadJsonFile(nodesFile, []);
|
|
1750
|
+
return Array.isArray(list) ? list : [];
|
|
1751
|
+
}
|
|
1752
|
+
function saveNodes(list) {
|
|
1753
|
+
return saveJsonFile(nodesFile, list);
|
|
1754
|
+
}
|
|
1755
|
+
function loadSources() {
|
|
1756
|
+
const list = loadJsonFile(sourcesFile, []);
|
|
1757
|
+
return Array.isArray(list) ? list : [];
|
|
1758
|
+
}
|
|
1759
|
+
function saveSources(list) {
|
|
1760
|
+
return saveJsonFile(sourcesFile, list);
|
|
1761
|
+
}
|
|
1762
|
+
function loadNodeData(nodeId, type, fallback) {
|
|
1763
|
+
const file = (0, path_1.join)(nodeDataDir, nodeId, `${type}.json`);
|
|
1764
|
+
return loadJsonFile(file, fallback);
|
|
1765
|
+
}
|
|
1766
|
+
function saveNodeData(nodeId, type, data) {
|
|
1767
|
+
const file = (0, path_1.join)(nodeDataDir, nodeId, `${type}.json`);
|
|
1768
|
+
return saveJsonFile(file, data);
|
|
1769
|
+
}
|
|
1770
|
+
// 测试节点连接
|
|
1771
|
+
async function testNodeConnection(node) {
|
|
1772
|
+
try {
|
|
1773
|
+
if (node.type === 'local') {
|
|
1774
|
+
const dir = (0, path_1.resolve)(node.url);
|
|
1775
|
+
if (!(0, fs_1.existsSync)(dir) || !(0, fs_1.statSync)(dir).isDirectory()) {
|
|
1776
|
+
return { ok: false, error: '本地目录不存在' };
|
|
1777
|
+
}
|
|
1778
|
+
const caps = [];
|
|
1779
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'templates.json')))
|
|
1780
|
+
caps.push('templates');
|
|
1781
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'skills.json')))
|
|
1782
|
+
caps.push('skills');
|
|
1783
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'office.json')))
|
|
1784
|
+
caps.push('office');
|
|
1785
|
+
return { ok: true, capabilities: caps.length ? caps : node.capabilities };
|
|
1786
|
+
}
|
|
1787
|
+
// http / git 类型:请求节点的 /api/health 或 manifest
|
|
1788
|
+
const url = node.type === 'http'
|
|
1789
|
+
? node.url.replace(/\/$/, '') + '/api/health'
|
|
1790
|
+
: node.url;
|
|
1791
|
+
const headers = { 'User-Agent': 'fhcode-node/1.0' };
|
|
1792
|
+
if (node.apiKey)
|
|
1793
|
+
headers['Authorization'] = 'Bearer ' + node.apiKey;
|
|
1794
|
+
const r = await fetchWithRetry(url, 10000, 0);
|
|
1795
|
+
if (!r.ok)
|
|
1796
|
+
return { ok: false, error: `HTTP ${r.status}` };
|
|
1797
|
+
if (node.type === 'http') {
|
|
1798
|
+
const data = await r.json().catch(() => ({}));
|
|
1799
|
+
return { ok: true, capabilities: Array.isArray(data.capabilities) ? data.capabilities : node.capabilities };
|
|
1800
|
+
}
|
|
1801
|
+
return { ok: true, capabilities: node.capabilities };
|
|
1802
|
+
}
|
|
1803
|
+
catch (e) {
|
|
1804
|
+
return { ok: false, error: e?.message || '连接失败' };
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
// 从节点同步数据
|
|
1808
|
+
async function syncNodeData(node) {
|
|
1809
|
+
const synced = [];
|
|
1810
|
+
try {
|
|
1811
|
+
if (node.type === 'local') {
|
|
1812
|
+
const dir = (0, path_1.resolve)(node.url);
|
|
1813
|
+
for (const cap of node.capabilities) {
|
|
1814
|
+
const file = (0, path_1.join)(dir, `${cap}.json`);
|
|
1815
|
+
if ((0, fs_1.existsSync)(file)) {
|
|
1816
|
+
const data = JSON.parse((0, fs_1.readFileSync)(file, 'utf8'));
|
|
1817
|
+
saveNodeData(node.id, cap, data);
|
|
1818
|
+
synced.push(cap);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return { ok: true, synced };
|
|
1822
|
+
}
|
|
1823
|
+
// http 类型
|
|
1824
|
+
if (node.type === 'http') {
|
|
1825
|
+
const base = node.url.replace(/\/$/, '');
|
|
1826
|
+
const headers = { 'User-Agent': 'fhcode-node/1.0' };
|
|
1827
|
+
if (node.apiKey)
|
|
1828
|
+
headers['Authorization'] = 'Bearer ' + node.apiKey;
|
|
1829
|
+
for (const cap of node.capabilities) {
|
|
1830
|
+
try {
|
|
1831
|
+
const r = await fetch(`${base}/api/node/${cap}`, { headers, signal: AbortSignal.timeout(15000) });
|
|
1832
|
+
if (r.ok) {
|
|
1833
|
+
const data = await r.json();
|
|
1834
|
+
const list = Array.isArray(data) ? data : (data.items || data.list || []);
|
|
1835
|
+
saveNodeData(node.id, cap, list);
|
|
1836
|
+
synced.push(cap);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
catch { /* skip */ }
|
|
1840
|
+
}
|
|
1841
|
+
return { ok: true, synced };
|
|
1842
|
+
}
|
|
1843
|
+
// git 类型:暂不支持自动 clone,标记为需手动同步
|
|
1844
|
+
return { ok: false, error: 'Git 节点暂不支持自动同步,请使用本地目录方式', synced };
|
|
1845
|
+
}
|
|
1846
|
+
catch (e) {
|
|
1847
|
+
return { ok: false, error: e?.message || '同步失败', synced };
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
// 节点 CRUD
|
|
1851
|
+
app.get('/api/nodes', (_req, res) => {
|
|
1852
|
+
const nodes = loadNodes().map((n) => {
|
|
1853
|
+
const { apiKey, ...rest } = n;
|
|
1854
|
+
return { ...rest, hasApiKey: !!apiKey };
|
|
1855
|
+
});
|
|
1856
|
+
res.json({ ok: true, nodes });
|
|
1857
|
+
});
|
|
1858
|
+
app.post('/api/nodes', async (req, res) => {
|
|
1859
|
+
const body = (req.body ?? {});
|
|
1860
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
1861
|
+
const type = ['http', 'git', 'local'].find((t) => t === body.type) || 'http';
|
|
1862
|
+
const url = typeof body.url === 'string' ? body.url.trim() : '';
|
|
1863
|
+
const apiKey = typeof body.apiKey === 'string' && body.apiKey.trim() ? body.apiKey.trim() : undefined;
|
|
1864
|
+
const capabilities = Array.isArray(body.capabilities) ? body.capabilities.filter((c) => ['templates', 'skills', 'office'].includes(c)) : ['templates', 'skills', 'office'];
|
|
1865
|
+
if (!name || !url) {
|
|
1866
|
+
res.status(400).json({ ok: false, error: '缺少 name 或 url 字段' });
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const node = {
|
|
1870
|
+
id: `node_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 4)}`,
|
|
1871
|
+
name, type, url, apiKey, capabilities,
|
|
1872
|
+
status: 'disconnected',
|
|
1873
|
+
enabled: true,
|
|
1874
|
+
createdAt: new Date().toISOString(),
|
|
1875
|
+
};
|
|
1876
|
+
const list = loadNodes();
|
|
1877
|
+
list.push(node);
|
|
1878
|
+
if (!saveNodes(list)) {
|
|
1879
|
+
res.status(500).json({ ok: false, error: '节点保存失败' });
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
// 自动测试连接
|
|
1883
|
+
const test = await testNodeConnection(node);
|
|
1884
|
+
node.status = test.ok ? 'connected' : 'error';
|
|
1885
|
+
node.lastError = test.error;
|
|
1886
|
+
if (test.capabilities)
|
|
1887
|
+
node.capabilities = test.capabilities;
|
|
1888
|
+
saveNodes(list);
|
|
1889
|
+
res.status(201).json({ ok: true, node: { ...node, apiKey: undefined, hasApiKey: !!apiKey }, connection: test });
|
|
1890
|
+
});
|
|
1891
|
+
app.put('/api/nodes/:id', async (req, res) => {
|
|
1892
|
+
const list = loadNodes();
|
|
1893
|
+
const idx = list.findIndex((n) => n.id === req.params.id);
|
|
1894
|
+
if (idx < 0) {
|
|
1895
|
+
res.status(404).json({ ok: false, error: '节点不存在' });
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
const body = (req.body ?? {});
|
|
1899
|
+
const node = list[idx];
|
|
1900
|
+
if (typeof body.name === 'string' && body.name.trim())
|
|
1901
|
+
node.name = body.name.trim();
|
|
1902
|
+
if (typeof body.url === 'string' && body.url.trim())
|
|
1903
|
+
node.url = body.url.trim();
|
|
1904
|
+
if (typeof body.type === 'string' && ['http', 'git', 'local'].includes(body.type))
|
|
1905
|
+
node.type = body.type;
|
|
1906
|
+
if (typeof body.apiKey === 'string')
|
|
1907
|
+
node.apiKey = body.apiKey.trim() ? body.apiKey.trim() : undefined;
|
|
1908
|
+
if (Array.isArray(body.capabilities))
|
|
1909
|
+
node.capabilities = body.capabilities.filter((c) => ['templates', 'skills', 'office'].includes(c));
|
|
1910
|
+
if (typeof body.enabled === 'boolean')
|
|
1911
|
+
node.enabled = body.enabled;
|
|
1912
|
+
if (!saveNodes(list)) {
|
|
1913
|
+
res.status(500).json({ ok: false, error: '节点更新失败' });
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
res.json({ ok: true, node: { ...node, apiKey: undefined, hasApiKey: !!node.apiKey } });
|
|
1917
|
+
});
|
|
1918
|
+
app.delete('/api/nodes/:id', (req, res) => {
|
|
1919
|
+
const list = loadNodes();
|
|
1920
|
+
const next = list.filter((n) => n.id !== req.params.id);
|
|
1921
|
+
if (next.length === list.length) {
|
|
1922
|
+
res.status(404).json({ ok: false, error: '节点不存在' });
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (!saveNodes(next)) {
|
|
1926
|
+
res.status(500).json({ ok: false, error: '节点删除失败' });
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
res.json({ ok: true });
|
|
1930
|
+
});
|
|
1931
|
+
app.post('/api/nodes/:id/test', async (req, res) => {
|
|
1932
|
+
const node = loadNodes().find((n) => n.id === req.params.id);
|
|
1933
|
+
if (!node) {
|
|
1934
|
+
res.status(404).json({ ok: false, error: '节点不存在' });
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
const test = await testNodeConnection(node);
|
|
1938
|
+
// 更新节点状态
|
|
1939
|
+
const list = loadNodes();
|
|
1940
|
+
const idx = list.findIndex((n) => n.id === node.id);
|
|
1941
|
+
if (idx >= 0) {
|
|
1942
|
+
list[idx].status = test.ok ? 'connected' : 'error';
|
|
1943
|
+
list[idx].lastError = test.error;
|
|
1944
|
+
if (test.capabilities)
|
|
1945
|
+
list[idx].capabilities = test.capabilities;
|
|
1946
|
+
saveNodes(list);
|
|
1947
|
+
}
|
|
1948
|
+
res.json({ ok: test.ok, error: test.error, capabilities: test.capabilities });
|
|
1949
|
+
});
|
|
1950
|
+
app.post('/api/nodes/:id/sync', async (req, res) => {
|
|
1951
|
+
const node = loadNodes().find((n) => n.id === req.params.id);
|
|
1952
|
+
if (!node) {
|
|
1953
|
+
res.status(404).json({ ok: false, error: '节点不存在' });
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const result = await syncNodeData(node);
|
|
1957
|
+
const list = loadNodes();
|
|
1958
|
+
const idx = list.findIndex((n) => n.id === node.id);
|
|
1959
|
+
if (idx >= 0) {
|
|
1960
|
+
list[idx].lastSyncAt = new Date().toISOString();
|
|
1961
|
+
list[idx].status = result.ok ? 'connected' : 'error';
|
|
1962
|
+
list[idx].lastError = result.error;
|
|
1963
|
+
saveNodes(list);
|
|
1964
|
+
}
|
|
1965
|
+
res.json({ ok: result.ok, error: result.error, synced: result.synced });
|
|
1966
|
+
});
|
|
1967
|
+
// 自定义来源 CRUD
|
|
1968
|
+
app.get('/api/sources', (_req, res) => {
|
|
1969
|
+
res.json({ ok: true, sources: loadSources() });
|
|
1970
|
+
});
|
|
1971
|
+
app.post('/api/sources', (req, res) => {
|
|
1972
|
+
const body = (req.body ?? {});
|
|
1973
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
1974
|
+
const type = ['templates', 'skills', 'office'].find((t) => t === body.type);
|
|
1975
|
+
const url = typeof body.url === 'string' ? body.url.trim() : '';
|
|
1976
|
+
const nodeId = typeof body.nodeId === 'string' && body.nodeId.trim() ? body.nodeId.trim() : undefined;
|
|
1977
|
+
if (!name || !type || !url) {
|
|
1978
|
+
res.status(400).json({ ok: false, error: '缺少 name / type / url 字段' });
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const source = {
|
|
1982
|
+
id: `src_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 4)}`,
|
|
1983
|
+
name, type, url, nodeId,
|
|
1984
|
+
enabled: true,
|
|
1985
|
+
createdAt: new Date().toISOString(),
|
|
1986
|
+
};
|
|
1987
|
+
const list = loadSources();
|
|
1988
|
+
list.push(source);
|
|
1989
|
+
if (!saveSources(list)) {
|
|
1990
|
+
res.status(500).json({ ok: false, error: '来源保存失败' });
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
res.status(201).json({ ok: true, source });
|
|
1994
|
+
});
|
|
1995
|
+
app.put('/api/sources/:id', (req, res) => {
|
|
1996
|
+
const list = loadSources();
|
|
1997
|
+
const idx = list.findIndex((s) => s.id === req.params.id);
|
|
1998
|
+
if (idx < 0) {
|
|
1999
|
+
res.status(404).json({ ok: false, error: '来源不存在' });
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
const body = (req.body ?? {});
|
|
2003
|
+
const source = list[idx];
|
|
2004
|
+
if (typeof body.name === 'string' && body.name.trim())
|
|
2005
|
+
source.name = body.name.trim();
|
|
2006
|
+
if (typeof body.url === 'string' && body.url.trim())
|
|
2007
|
+
source.url = body.url.trim();
|
|
2008
|
+
if (typeof body.enabled === 'boolean')
|
|
2009
|
+
source.enabled = body.enabled;
|
|
2010
|
+
if (typeof body.nodeId === 'string')
|
|
2011
|
+
source.nodeId = body.nodeId.trim() || undefined;
|
|
2012
|
+
if (!saveSources(list)) {
|
|
2013
|
+
res.status(500).json({ ok: false, error: '来源更新失败' });
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
res.json({ ok: true, source });
|
|
2017
|
+
});
|
|
2018
|
+
app.delete('/api/sources/:id', (req, res) => {
|
|
2019
|
+
const list = loadSources();
|
|
2020
|
+
const next = list.filter((s) => s.id !== req.params.id);
|
|
2021
|
+
if (next.length === list.length) {
|
|
2022
|
+
res.status(404).json({ ok: false, error: '来源不存在' });
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
if (!saveSources(next)) {
|
|
2026
|
+
res.status(500).json({ ok: false, error: '来源删除失败' });
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
res.json({ ok: true });
|
|
2030
|
+
});
|
|
2031
|
+
// 从所有启用的节点收集数据
|
|
2032
|
+
function collectNodeTemplates() {
|
|
2033
|
+
const result = [];
|
|
2034
|
+
for (const node of loadNodes()) {
|
|
2035
|
+
if (!node.enabled || !node.capabilities.includes('templates'))
|
|
2036
|
+
continue;
|
|
2037
|
+
const data = loadNodeData(node.id, 'templates', []);
|
|
2038
|
+
if (Array.isArray(data)) {
|
|
2039
|
+
result.push(...data.map((t) => ({ ...t, source: node.name, nodeId: node.id })));
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return result;
|
|
2043
|
+
}
|
|
2044
|
+
function collectNodeSkills() {
|
|
2045
|
+
const result = [];
|
|
2046
|
+
for (const node of loadNodes()) {
|
|
2047
|
+
if (!node.enabled || !node.capabilities.includes('skills'))
|
|
2048
|
+
continue;
|
|
2049
|
+
const data = loadNodeData(node.id, 'skills', []);
|
|
2050
|
+
if (Array.isArray(data)) {
|
|
2051
|
+
result.push(...data.map((s) => ({ ...s, source: node.name, nodeId: node.id })));
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return result;
|
|
2055
|
+
}
|
|
2056
|
+
function collectNodeOffice() {
|
|
2057
|
+
const result = [];
|
|
2058
|
+
for (const node of loadNodes()) {
|
|
2059
|
+
if (!node.enabled || !node.capabilities.includes('office'))
|
|
2060
|
+
continue;
|
|
2061
|
+
const data = loadNodeData(node.id, 'office', []);
|
|
2062
|
+
if (Array.isArray(data)) {
|
|
2063
|
+
result.push(...data.map((c) => ({ ...c, source: node.name, nodeId: node.id })));
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return result;
|
|
2067
|
+
}
|
|
2068
|
+
/* ========== 技能市场(插件市场):聚合 ClawHub + Agent-Foundry + 自定义节点 + 自定义来源 ========== */
|
|
2069
|
+
const CLAWHUB_API = 'https://clawhub.ai/api/v1/skills';
|
|
2070
|
+
const AGENT_FOUNDRY_CATALOG = 'https://raw.githubusercontent.com/hebertzhu/agent-foundry/main/catalog/skills-catalog.json';
|
|
2071
|
+
async function fetchWithRetry(url, timeoutMs = 15000, retries = 1) {
|
|
2072
|
+
let lastErr;
|
|
2073
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
2074
|
+
const ctrl = new AbortController();
|
|
2075
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
2076
|
+
try {
|
|
2077
|
+
const r = await fetch(url, {
|
|
2078
|
+
signal: ctrl.signal,
|
|
2079
|
+
headers: { 'User-Agent': 'fhcode-web/0.5.1' },
|
|
2080
|
+
});
|
|
2081
|
+
clearTimeout(timer);
|
|
2082
|
+
return r;
|
|
2083
|
+
}
|
|
2084
|
+
catch (e) {
|
|
2085
|
+
clearTimeout(timer);
|
|
2086
|
+
lastErr = e;
|
|
2087
|
+
if (attempt < retries)
|
|
2088
|
+
await new Promise((res) => setTimeout(res, 500));
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
throw lastErr;
|
|
2092
|
+
}
|
|
2093
|
+
async function fetchClawHubSkills(keyword, limit) {
|
|
2094
|
+
try {
|
|
2095
|
+
const url = `${CLAWHUB_API}?limit=${limit}${keyword ? `&q=${encodeURIComponent(keyword)}` : ''}`;
|
|
2096
|
+
const r = await fetchWithRetry(url);
|
|
2097
|
+
if (!r.ok)
|
|
2098
|
+
return [];
|
|
2099
|
+
const data = (await r.json());
|
|
2100
|
+
return (data.items || []).map((it) => ({
|
|
2101
|
+
id: `clawhub:${String(it.slug || '')}`,
|
|
2102
|
+
name: String(it.displayName || it.slug || ''),
|
|
2103
|
+
description: String(it.summary || it.description || ''),
|
|
2104
|
+
source: 'clawhub',
|
|
2105
|
+
author: it.slug ? String(it.slug).split('/')[0] : undefined,
|
|
2106
|
+
category: 'market',
|
|
2107
|
+
tags: Array.isArray(it.topics) ? it.topics : [],
|
|
2108
|
+
downloads: Number(it.stats?.downloads ?? 0),
|
|
2109
|
+
installHint: `npx skills add ${String(it.slug || '')}`,
|
|
2110
|
+
homepage: `https://clawhub.ai/${String(it.slug || '')}`,
|
|
2111
|
+
rawUrl: `https://clawhub.ai/${String(it.slug || '')}`,
|
|
2112
|
+
}));
|
|
2113
|
+
}
|
|
2114
|
+
catch (e) {
|
|
2115
|
+
console.error('[clawhub] fetch failed:', e);
|
|
2116
|
+
return [];
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
async function fetchAgentFoundrySkills(keyword, limit) {
|
|
2120
|
+
try {
|
|
2121
|
+
const r = await fetchWithRetry(AGENT_FOUNDRY_CATALOG);
|
|
2122
|
+
if (!r.ok)
|
|
2123
|
+
return [];
|
|
2124
|
+
const data = (await r.json());
|
|
2125
|
+
const arr = Array.isArray(data.skills) ? data.skills : [];
|
|
2126
|
+
let list = arr.map((it) => {
|
|
2127
|
+
const name = String(it.name || '');
|
|
2128
|
+
const publicPath = String(it.public_path || '');
|
|
2129
|
+
const category = String(it.category || 'market');
|
|
2130
|
+
const rawUrl = publicPath
|
|
2131
|
+
? `https://raw.githubusercontent.com/hebertzhu/agent-foundry/main/${publicPath}`
|
|
2132
|
+
: `https://github.com/hebertzhu/agent-foundry`;
|
|
2133
|
+
return {
|
|
2134
|
+
id: `agent-foundry:${name}`,
|
|
2135
|
+
name,
|
|
2136
|
+
description: `Agent-Foundry 技能包 · 分类:${category}${publicPath ? ` · 源:${publicPath}` : ''}`,
|
|
2137
|
+
source: 'agent-foundry',
|
|
2138
|
+
author: 'hebertzhu',
|
|
2139
|
+
category,
|
|
2140
|
+
tags: Array.isArray(it.packs) ? it.packs : [category],
|
|
2141
|
+
downloads: 0,
|
|
2142
|
+
installHint: `npm i -g @agent-foundry/cli && agent-foundry install ${name}`,
|
|
2143
|
+
homepage: `https://github.com/hebertzhu/agent-foundry`,
|
|
2144
|
+
rawUrl,
|
|
2145
|
+
};
|
|
2146
|
+
});
|
|
2147
|
+
if (keyword) {
|
|
2148
|
+
const kw = keyword.toLowerCase();
|
|
2149
|
+
list = list.filter((s) => s.name.toLowerCase().includes(kw) ||
|
|
2150
|
+
s.description.toLowerCase().includes(kw) ||
|
|
2151
|
+
(s.tags || []).some((t) => t.toLowerCase().includes(kw)));
|
|
2152
|
+
}
|
|
2153
|
+
return list.slice(0, limit);
|
|
2154
|
+
}
|
|
2155
|
+
catch (e) {
|
|
2156
|
+
console.error('[agent-foundry] fetch failed:', e);
|
|
2157
|
+
return [];
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
app.get('/api/skills/market', async (req, res) => {
|
|
2161
|
+
const q = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
2162
|
+
const source = typeof req.query.source === 'string' ? req.query.source : 'all';
|
|
2163
|
+
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
|
2164
|
+
let results = [];
|
|
2165
|
+
if (source === 'clawhub' || source === 'all')
|
|
2166
|
+
results = results.concat(await fetchClawHubSkills(q, limit));
|
|
2167
|
+
if (source === 'agent-foundry' || source === 'all')
|
|
2168
|
+
results = results.concat(await fetchAgentFoundrySkills(q, limit));
|
|
2169
|
+
// 从节点同步的技能
|
|
2170
|
+
if (source === 'node' || source === 'all') {
|
|
2171
|
+
const nodeSkills = collectNodeSkills();
|
|
2172
|
+
results = results.concat(nodeSkills.map((s) => ({
|
|
2173
|
+
id: `node:${s.id}`,
|
|
2174
|
+
name: s.name,
|
|
2175
|
+
description: s.description,
|
|
2176
|
+
source: s.source,
|
|
2177
|
+
author: s.author,
|
|
2178
|
+
category: s.category,
|
|
2179
|
+
tags: s.tags,
|
|
2180
|
+
downloads: s.downloads,
|
|
2181
|
+
installHint: s.installHint,
|
|
2182
|
+
homepage: s.homepage,
|
|
2183
|
+
rawUrl: s.rawUrl,
|
|
2184
|
+
})));
|
|
2185
|
+
}
|
|
2186
|
+
// 从自定义来源拉取技能(HTTP URL 返回 JSON 数组)
|
|
2187
|
+
if (source === 'custom' || source === 'all') {
|
|
2188
|
+
for (const src of loadSources()) {
|
|
2189
|
+
if (!src.enabled || src.type !== 'skills')
|
|
2190
|
+
continue;
|
|
2191
|
+
try {
|
|
2192
|
+
const r = await fetchWithRetry(src.url, 10000, 0);
|
|
2193
|
+
if (r.ok) {
|
|
2194
|
+
const data = await r.json();
|
|
2195
|
+
const list = Array.isArray(data) ? data : (data.items || data.skills || []);
|
|
2196
|
+
results = results.concat(list.map((s) => ({
|
|
2197
|
+
id: `custom:${src.id}:${s.id || s.name}`,
|
|
2198
|
+
name: String(s.name || s.title || ''),
|
|
2199
|
+
description: String(s.description || s.desc || ''),
|
|
2200
|
+
source: src.name,
|
|
2201
|
+
author: s.author,
|
|
2202
|
+
category: String(s.category || 'custom'),
|
|
2203
|
+
tags: Array.isArray(s.tags) ? s.tags : [],
|
|
2204
|
+
downloads: Number(s.downloads || 0),
|
|
2205
|
+
installHint: String(s.installHint || ''),
|
|
2206
|
+
homepage: String(s.homepage || src.url),
|
|
2207
|
+
rawUrl: String(s.rawUrl || src.url),
|
|
2208
|
+
})));
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
catch { /* skip failed sources */ }
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
// 关键词过滤
|
|
2215
|
+
if (q) {
|
|
2216
|
+
const kw = q.toLowerCase();
|
|
2217
|
+
results = results.filter((s) => s.name.toLowerCase().includes(kw) ||
|
|
2218
|
+
s.description.toLowerCase().includes(kw) ||
|
|
2219
|
+
(s.tags || []).some((t) => t.toLowerCase().includes(kw)));
|
|
2220
|
+
}
|
|
2221
|
+
results.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
|
2222
|
+
res.json({ ok: true, total: results.length, skills: results.slice(0, limit) });
|
|
2223
|
+
});
|
|
2224
|
+
// 已安装技能持久化
|
|
2225
|
+
const installedSkillsFile = (0, path_1.join)(homeDir, 'skills', 'installed.json');
|
|
2226
|
+
app.post('/api/skills/install', async (req, res) => {
|
|
2227
|
+
const body = req.body;
|
|
2228
|
+
if (!body || !body.id || !body.name || !body.source) {
|
|
2229
|
+
res.status(400).json({ ok: false, error: '缺少 id / name / source 字段' });
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
try {
|
|
2233
|
+
const dir = (0, path_1.join)(homeDir, 'skills', body.id.replace(/[^a-zA-Z0-9_-]/g, '_'));
|
|
2234
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
2235
|
+
if (!saveJsonFile((0, path_1.join)(dir, 'meta.json'), { ...body, installedAt: new Date().toISOString() })) {
|
|
2236
|
+
res.status(500).json({ ok: false, error: '技能元数据写入失败' });
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const installed = loadJsonFile(installedSkillsFile, []);
|
|
2240
|
+
if (!installed.find((s) => s.id === body.id)) {
|
|
2241
|
+
installed.push(body);
|
|
2242
|
+
if (!saveJsonFile(installedSkillsFile, installed)) {
|
|
2243
|
+
res.status(500).json({ ok: false, error: '已安装列表写入失败' });
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
res.json({ ok: true, message: `技能「${body.name}」已记录为已安装` });
|
|
2248
|
+
}
|
|
2249
|
+
catch (e) {
|
|
2250
|
+
res.status(500).json({ ok: false, error: '安装失败: ' + e.message });
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
app.get('/api/skills/installed', (_req, res) => {
|
|
2254
|
+
res.json({ ok: true, skills: loadJsonFile(installedSkillsFile, []) });
|
|
2255
|
+
});
|
|
2256
|
+
// 预置常用快捷指令(用户可一键运行,也可保存为自己的指令后编辑)
|
|
2257
|
+
const BUILTIN_AUTOMATIONS = [
|
|
2258
|
+
{
|
|
2259
|
+
id: 'builtin-code-review',
|
|
2260
|
+
name: '代码审查',
|
|
2261
|
+
icon: '🔍',
|
|
2262
|
+
category: '质量',
|
|
2263
|
+
goal: '请对当前工作区的代码做一次全面审查,重点检查:1) 潜在的 Bug 和逻辑漏洞;2) 代码规范和可读性问题;3) 安全风险;4) 性能瓶颈。按问题严重程度排序,给出具体的修改建议。',
|
|
2264
|
+
builtin: true,
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
id: 'builtin-gen-test',
|
|
2268
|
+
name: '生成测试',
|
|
2269
|
+
icon: '🧪',
|
|
2270
|
+
category: '测试',
|
|
2271
|
+
goal: '请为当前项目生成单元测试,优先覆盖核心业务逻辑和边界条件。先分析项目结构和已有测试框架,然后为关键模块编写测试用例,确保测试可以直接运行。',
|
|
2272
|
+
builtin: true,
|
|
2273
|
+
},
|
|
2274
|
+
{
|
|
2275
|
+
id: 'builtin-build-check',
|
|
2276
|
+
name: '构建检查',
|
|
2277
|
+
icon: '🏗️',
|
|
2278
|
+
category: '工程',
|
|
2279
|
+
goal: '请运行项目的构建命令(如 npm run build / tsc),检查是否有编译错误或类型错误。如果有错误,逐一分析原因并给出修复方案,修复后重新验证构建是否通过。',
|
|
2280
|
+
builtin: true,
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
id: 'builtin-dep-check',
|
|
2284
|
+
name: '依赖检查',
|
|
2285
|
+
icon: '📦',
|
|
2286
|
+
category: '工程',
|
|
2287
|
+
goal: '请检查当前项目的 package.json 依赖,分析:1) 是否有过时的依赖需要升级;2) 是否有已知安全漏洞的依赖;3) 是否有未使用的冗余依赖。给出升级建议和风险提示。',
|
|
2288
|
+
builtin: true,
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
id: 'builtin-format',
|
|
2292
|
+
name: '代码格式化',
|
|
2293
|
+
icon: '✨',
|
|
2294
|
+
category: '工程',
|
|
2295
|
+
goal: '请对当前项目的代码进行全面格式化。先检查项目是否配置了 ESLint / Prettier 等工具,如果有就按配置格式化;如果没有,就按通用规范统一缩进、引号、分号等风格。格式化后验证构建是否正常。',
|
|
2296
|
+
builtin: true,
|
|
2297
|
+
},
|
|
2298
|
+
{
|
|
2299
|
+
id: 'builtin-gen-doc',
|
|
2300
|
+
name: '生成文档',
|
|
2301
|
+
icon: '📝',
|
|
2302
|
+
category: '文档',
|
|
2303
|
+
goal: '请为当前项目生成一份完整的 README 文档,包含:项目简介、功能特性、技术栈、安装步骤、使用方法、目录结构说明、开发指南。如果项目有 API,也一并生成 API 文档。',
|
|
2304
|
+
builtin: true,
|
|
2305
|
+
},
|
|
2306
|
+
{
|
|
2307
|
+
id: 'builtin-refactor',
|
|
2308
|
+
name: '重构优化',
|
|
2309
|
+
icon: '♻️',
|
|
2310
|
+
category: '质量',
|
|
2311
|
+
goal: '请对当前项目的代码进行重构优化,重点关注:1) 重复代码提取;2) 过长函数拆分;3) 命名规范统一;4) 复杂逻辑简化。重构过程中确保不改变外部行为,重构后验证构建和测试通过。',
|
|
2312
|
+
builtin: true,
|
|
2313
|
+
},
|
|
2314
|
+
{
|
|
2315
|
+
id: 'builtin-release',
|
|
2316
|
+
name: '版本发布准备',
|
|
2317
|
+
icon: '🚀',
|
|
2318
|
+
category: '工程',
|
|
2319
|
+
goal: '请为当前项目准备一次版本发布,包括:1) 检查 package.json 版本号并建议升级;2) 整理 CHANGELOG 变更日志;3) 运行构建和测试确保通过;4) 检查是否有未提交的代码。给出完整的发布检查清单。',
|
|
2320
|
+
builtin: true,
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
id: 'builtin-security',
|
|
2324
|
+
name: '安全扫描',
|
|
2325
|
+
icon: '🛡️',
|
|
2326
|
+
category: '安全',
|
|
2327
|
+
goal: '请对当前项目的代码进行安全扫描,重点检查:1) SQL 注入、XSS、CSRF 等常见漏洞;2) 硬编码的密钥或敏感信息;3) 不安全的依赖;4) 权限校验缺失。按风险等级排序,给出修复建议。',
|
|
2328
|
+
builtin: true,
|
|
2329
|
+
},
|
|
2330
|
+
{
|
|
2331
|
+
id: 'builtin-perf',
|
|
2332
|
+
name: '性能分析',
|
|
2333
|
+
icon: '⚡',
|
|
2334
|
+
category: '质量',
|
|
2335
|
+
goal: '请分析当前项目的性能瓶颈,重点关注:1) 慢查询和低效算法;2) 不必要的重复计算;3) 内存泄漏风险;4) 异步操作阻塞。给出具体的优化方案和预期收益,优化后验证效果。',
|
|
2336
|
+
builtin: true,
|
|
2337
|
+
},
|
|
2338
|
+
];
|
|
2339
|
+
const automationsFile = (0, path_1.join)(homeDir, 'automations.json');
|
|
2340
|
+
app.get('/api/automations', (_req, res) => {
|
|
2341
|
+
const user = loadJsonFile(automationsFile, []);
|
|
2342
|
+
res.json({ ok: true, builtin: BUILTIN_AUTOMATIONS, automations: user });
|
|
2343
|
+
});
|
|
2344
|
+
app.post('/api/automations', async (req, res) => {
|
|
2345
|
+
const body = (req.body ?? {});
|
|
2346
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
2347
|
+
const goal = typeof body.goal === 'string' ? body.goal.trim() : '';
|
|
2348
|
+
if (!name || !goal) {
|
|
2349
|
+
res.status(400).json({ ok: false, error: '缺少 name 或 goal 字段' });
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
const list = loadJsonFile(automationsFile, []);
|
|
2353
|
+
const rule = {
|
|
628
2354
|
id: `auto_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 4)}`,
|
|
629
2355
|
name,
|
|
630
2356
|
goal,
|
|
@@ -658,7 +2384,16 @@ function startWebServer(opts = {}) {
|
|
|
658
2384
|
});
|
|
659
2385
|
app.post('/api/automations/:id/run', async (req, res) => {
|
|
660
2386
|
const list = loadJsonFile(automationsFile, []);
|
|
661
|
-
|
|
2387
|
+
// 先从用户自定义指令里找,找不到再从预置指令里找
|
|
2388
|
+
let rule = list.find((a) => a.id === req.params.id);
|
|
2389
|
+
let isBuiltin = false;
|
|
2390
|
+
if (!rule) {
|
|
2391
|
+
const builtin = BUILTIN_AUTOMATIONS.find((a) => a.id === req.params.id);
|
|
2392
|
+
if (builtin) {
|
|
2393
|
+
rule = builtin;
|
|
2394
|
+
isBuiltin = true;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
662
2397
|
if (!rule) {
|
|
663
2398
|
res.status(404).json({ ok: false, error: '指令不存在' });
|
|
664
2399
|
return;
|
|
@@ -669,12 +2404,16 @@ function startWebServer(opts = {}) {
|
|
|
669
2404
|
});
|
|
670
2405
|
rule.runCount += 1;
|
|
671
2406
|
rule.lastRunAt = new Date().toISOString();
|
|
672
|
-
//
|
|
673
|
-
|
|
2407
|
+
// 预置指令的 runCount 不持久化(只在内存中统计),用户自定义指令才持久化
|
|
2408
|
+
let persistWarning = false;
|
|
2409
|
+
if (!isBuiltin) {
|
|
2410
|
+
persistWarning = !saveJsonFile(automationsFile, list);
|
|
2411
|
+
}
|
|
674
2412
|
res.status(201).json({
|
|
675
2413
|
ok: true,
|
|
676
2414
|
task: publicTask(record, true),
|
|
677
2415
|
runCount: rule.runCount,
|
|
2416
|
+
builtin: isBuiltin || undefined,
|
|
678
2417
|
persistWarning: persistWarning || undefined,
|
|
679
2418
|
});
|
|
680
2419
|
});
|
|
@@ -737,9 +2476,43 @@ function startWebServer(opts = {}) {
|
|
|
737
2476
|
},
|
|
738
2477
|
];
|
|
739
2478
|
const templatesFile = (0, path_1.join)(homeDir, 'templates.json');
|
|
740
|
-
app.get('/api/templates', (_req, res) => {
|
|
2479
|
+
app.get('/api/templates', async (_req, res) => {
|
|
741
2480
|
const user = loadJsonFile(templatesFile, []);
|
|
742
|
-
|
|
2481
|
+
// 从节点同步的模板
|
|
2482
|
+
const nodeTemplates = collectNodeTemplates().map((t) => ({
|
|
2483
|
+
id: `node:${t.id}`,
|
|
2484
|
+
title: t.title,
|
|
2485
|
+
category: t.category,
|
|
2486
|
+
goal: t.goal,
|
|
2487
|
+
icon: t.icon,
|
|
2488
|
+
builtin: false,
|
|
2489
|
+
source: t.source,
|
|
2490
|
+
nodeId: t.nodeId,
|
|
2491
|
+
}));
|
|
2492
|
+
// 从自定义来源拉取模板
|
|
2493
|
+
const customTemplates = [];
|
|
2494
|
+
for (const src of loadSources()) {
|
|
2495
|
+
if (!src.enabled || src.type !== 'templates')
|
|
2496
|
+
continue;
|
|
2497
|
+
try {
|
|
2498
|
+
const r = await fetchWithRetry(src.url, 10000, 0);
|
|
2499
|
+
if (r.ok) {
|
|
2500
|
+
const data = await r.json();
|
|
2501
|
+
const list = Array.isArray(data) ? data : (data.items || data.templates || []);
|
|
2502
|
+
customTemplates.push(...list.map((t) => ({
|
|
2503
|
+
id: `custom:${src.id}:${t.id || t.title}`,
|
|
2504
|
+
title: String(t.title || t.name || ''),
|
|
2505
|
+
category: String(t.category || src.name),
|
|
2506
|
+
goal: String(t.goal || t.prompt || ''),
|
|
2507
|
+
icon: String(t.icon || '📄'),
|
|
2508
|
+
builtin: false,
|
|
2509
|
+
source: src.name,
|
|
2510
|
+
})));
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
catch { /* skip */ }
|
|
2514
|
+
}
|
|
2515
|
+
res.json({ ok: true, builtin: BUILTIN_TEMPLATES, user, node: nodeTemplates, custom: customTemplates });
|
|
743
2516
|
});
|
|
744
2517
|
app.post('/api/templates', async (req, res) => {
|
|
745
2518
|
const body = (req.body ?? {});
|
|
@@ -778,54 +2551,96 @@ function startWebServer(opts = {}) {
|
|
|
778
2551
|
}
|
|
779
2552
|
res.json({ ok: true });
|
|
780
2553
|
});
|
|
781
|
-
/* ========== 办公助理:内置能力清单 ========== */
|
|
782
|
-
app.get('/api/office/capabilities', (_req, res) => {
|
|
2554
|
+
/* ========== 办公助理:内置能力清单 + 节点能力 + 自定义来源 ========== */
|
|
2555
|
+
app.get('/api/office/capabilities', async (_req, res) => {
|
|
2556
|
+
const builtin = [
|
|
2557
|
+
{
|
|
2558
|
+
id: 'doc-summary',
|
|
2559
|
+
icon: '📄',
|
|
2560
|
+
title: '文档摘要',
|
|
2561
|
+
desc: '粘贴长文档,生成结构化摘要与要点',
|
|
2562
|
+
prompt: '请对以下文档做摘要,提取 3-5 个核心要点并列出待办:\n\n',
|
|
2563
|
+
source: '内置',
|
|
2564
|
+
},
|
|
2565
|
+
{
|
|
2566
|
+
id: 'doc-translate',
|
|
2567
|
+
icon: '🌐',
|
|
2568
|
+
title: '中英互译',
|
|
2569
|
+
desc: '技术文档/代码注释翻译',
|
|
2570
|
+
prompt: '请将以下内容准确翻译(保留代码与术语):\n\n',
|
|
2571
|
+
source: '内置',
|
|
2572
|
+
},
|
|
2573
|
+
{
|
|
2574
|
+
id: 'doc-rewrite',
|
|
2575
|
+
icon: '✍️',
|
|
2576
|
+
title: '润色改写',
|
|
2577
|
+
desc: '把草稿改写为正式/简洁风格',
|
|
2578
|
+
prompt: '请润色以下文本,使其更专业简洁:\n\n',
|
|
2579
|
+
source: '内置',
|
|
2580
|
+
},
|
|
2581
|
+
{
|
|
2582
|
+
id: 'meeting-notes',
|
|
2583
|
+
icon: '📋',
|
|
2584
|
+
title: '会议纪要',
|
|
2585
|
+
desc: '从聊天/录音转写生成行动项',
|
|
2586
|
+
prompt: '请从以下记录中提取:决策、负责人、截止时间、待办:\n\n',
|
|
2587
|
+
source: '内置',
|
|
2588
|
+
},
|
|
2589
|
+
{
|
|
2590
|
+
id: 'email-draft',
|
|
2591
|
+
icon: '✉️',
|
|
2592
|
+
title: '邮件起草',
|
|
2593
|
+
desc: '按要点生成工作邮件',
|
|
2594
|
+
prompt: '请起草一封邮件,主题/背景/诉求如下:\n\n',
|
|
2595
|
+
source: '内置',
|
|
2596
|
+
},
|
|
2597
|
+
{
|
|
2598
|
+
id: 'excel-formula',
|
|
2599
|
+
icon: '📊',
|
|
2600
|
+
title: '表格公式',
|
|
2601
|
+
desc: '描述需求生成 Excel/Sheets 公式',
|
|
2602
|
+
prompt: '请写出实现以下需求的表格公式并解释:\n\n',
|
|
2603
|
+
source: '内置',
|
|
2604
|
+
},
|
|
2605
|
+
];
|
|
2606
|
+
// 从节点同步的办公能力
|
|
2607
|
+
const nodeCaps = collectNodeOffice().map((c) => ({
|
|
2608
|
+
id: `node:${c.id}`,
|
|
2609
|
+
icon: c.icon,
|
|
2610
|
+
title: c.title,
|
|
2611
|
+
desc: c.desc,
|
|
2612
|
+
prompt: c.prompt,
|
|
2613
|
+
source: c.source,
|
|
2614
|
+
nodeId: c.nodeId,
|
|
2615
|
+
}));
|
|
2616
|
+
// 从自定义来源拉取办公能力
|
|
2617
|
+
const customCaps = [];
|
|
2618
|
+
for (const src of loadSources()) {
|
|
2619
|
+
if (!src.enabled || src.type !== 'office')
|
|
2620
|
+
continue;
|
|
2621
|
+
try {
|
|
2622
|
+
const r = await fetchWithRetry(src.url, 10000, 0);
|
|
2623
|
+
if (r.ok) {
|
|
2624
|
+
const data = await r.json();
|
|
2625
|
+
const list = Array.isArray(data) ? data : (data.items || data.capabilities || []);
|
|
2626
|
+
customCaps.push(...list.map((c) => ({
|
|
2627
|
+
id: `custom:${src.id}:${c.id || c.title}`,
|
|
2628
|
+
icon: String(c.icon || '🔧'),
|
|
2629
|
+
title: String(c.title || c.name || ''),
|
|
2630
|
+
desc: String(c.desc || c.description || ''),
|
|
2631
|
+
prompt: String(c.prompt || c.goal || ''),
|
|
2632
|
+
source: src.name,
|
|
2633
|
+
})));
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
catch { /* skip */ }
|
|
2637
|
+
}
|
|
783
2638
|
res.json({
|
|
784
2639
|
ok: true,
|
|
785
|
-
capabilities: [
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
title: '文档摘要',
|
|
790
|
-
desc: '粘贴长文档,生成结构化摘要与要点',
|
|
791
|
-
prompt: '请对以下文档做摘要,提取 3-5 个核心要点并列出待办:\n\n',
|
|
792
|
-
},
|
|
793
|
-
{
|
|
794
|
-
id: 'doc-translate',
|
|
795
|
-
icon: '🌐',
|
|
796
|
-
title: '中英互译',
|
|
797
|
-
desc: '技术文档/代码注释翻译',
|
|
798
|
-
prompt: '请将以下内容准确翻译(保留代码与术语):\n\n',
|
|
799
|
-
},
|
|
800
|
-
{
|
|
801
|
-
id: 'doc-rewrite',
|
|
802
|
-
icon: '✍️',
|
|
803
|
-
title: '润色改写',
|
|
804
|
-
desc: '把草稿改写为正式/简洁风格',
|
|
805
|
-
prompt: '请润色以下文本,使其更专业简洁:\n\n',
|
|
806
|
-
},
|
|
807
|
-
{
|
|
808
|
-
id: 'meeting-notes',
|
|
809
|
-
icon: '📋',
|
|
810
|
-
title: '会议纪要',
|
|
811
|
-
desc: '从聊天/录音转写生成行动项',
|
|
812
|
-
prompt: '请从以下记录中提取:决策、负责人、截止时间、待办:\n\n',
|
|
813
|
-
},
|
|
814
|
-
{
|
|
815
|
-
id: 'email-draft',
|
|
816
|
-
icon: '✉️',
|
|
817
|
-
title: '邮件起草',
|
|
818
|
-
desc: '按要点生成工作邮件',
|
|
819
|
-
prompt: '请起草一封邮件,主题/背景/诉求如下:\n\n',
|
|
820
|
-
},
|
|
821
|
-
{
|
|
822
|
-
id: 'excel-formula',
|
|
823
|
-
icon: '📊',
|
|
824
|
-
title: '表格公式',
|
|
825
|
-
desc: '描述需求生成 Excel/Sheets 公式',
|
|
826
|
-
prompt: '请写出实现以下需求的表格公式并解释:\n\n',
|
|
827
|
-
},
|
|
828
|
-
],
|
|
2640
|
+
capabilities: [...builtin, ...nodeCaps, ...customCaps],
|
|
2641
|
+
builtin,
|
|
2642
|
+
node: nodeCaps,
|
|
2643
|
+
custom: customCaps,
|
|
829
2644
|
});
|
|
830
2645
|
});
|
|
831
2646
|
/* ========== 大模型配置(三重加密:apiKey AES-256-GCM 落盘加密 + RSA 加密传输) ========== */
|
|
@@ -856,6 +2671,8 @@ function startWebServer(opts = {}) {
|
|
|
856
2671
|
apiKey: resolveApiKey(m),
|
|
857
2672
|
model: m.name,
|
|
858
2673
|
})));
|
|
2674
|
+
// P2: 同步更新补全引擎
|
|
2675
|
+
rebuildCompletionEngine(encList);
|
|
859
2676
|
return ok;
|
|
860
2677
|
}
|
|
861
2678
|
// 初始化模型提供列表,并注册到任务队列
|
|
@@ -867,6 +2684,52 @@ function startWebServer(opts = {}) {
|
|
|
867
2684
|
apiKey: resolveApiKey(m),
|
|
868
2685
|
model: m.name,
|
|
869
2686
|
})));
|
|
2687
|
+
/* ========== P2: 共享 ModelRouter + CompletionEngine(供 /api/completion 使用) ========== */
|
|
2688
|
+
let sharedModelRouter = null;
|
|
2689
|
+
let completionEngine = null;
|
|
2690
|
+
// P2-1: 设计稿转代码引擎(多模态)
|
|
2691
|
+
let designToCodeEngine = null;
|
|
2692
|
+
function rebuildCompletionEngine(models) {
|
|
2693
|
+
try {
|
|
2694
|
+
const providers = models
|
|
2695
|
+
.filter((m) => m.apiBase && m.name)
|
|
2696
|
+
.map((m) => new openai_compatible_provider_1.OpenAICompatibleProvider({
|
|
2697
|
+
id: m.id,
|
|
2698
|
+
type: 'openai-compatible',
|
|
2699
|
+
baseURL: m.apiBase,
|
|
2700
|
+
apiKey: resolveApiKey(m),
|
|
2701
|
+
model: m.name,
|
|
2702
|
+
tags: ['code-gen', 'cheap'],
|
|
2703
|
+
}));
|
|
2704
|
+
if (!providers.length) {
|
|
2705
|
+
sharedModelRouter = null;
|
|
2706
|
+
completionEngine = null;
|
|
2707
|
+
designToCodeEngine = null;
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
sharedModelRouter = new model_router_1.ModelRouter(providers, 'capability', 1.0);
|
|
2711
|
+
// 阶段一-1:构建代码图谱,用于跨文件上下文注入
|
|
2712
|
+
const codeGraph = (0, symbol_index_1.buildCodeGraph)(serverWorkspaceDir, { maxFiles: 500 });
|
|
2713
|
+
completionEngine = (0, completion_engine_1.createCompletionEngine)(sharedModelRouter, {
|
|
2714
|
+
maxPrefixChars: 1500,
|
|
2715
|
+
maxSuffixChars: 500,
|
|
2716
|
+
quickMaxTokens: 64,
|
|
2717
|
+
fullMaxTokens: 256,
|
|
2718
|
+
cacheTtlMs: 10000,
|
|
2719
|
+
enableSyntaxCheck: true,
|
|
2720
|
+
}, codeGraph);
|
|
2721
|
+
// P2-1: 更新设计稿转代码引擎的模型提供者
|
|
2722
|
+
designToCodeEngine = (0, design_to_code_1.createDesignToCodeEngine)(providers);
|
|
2723
|
+
}
|
|
2724
|
+
catch (e) {
|
|
2725
|
+
console.error('[completion] rebuild failed:', e);
|
|
2726
|
+
sharedModelRouter = null;
|
|
2727
|
+
completionEngine = null;
|
|
2728
|
+
designToCodeEngine = null;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
// 初始化时构建
|
|
2732
|
+
rebuildCompletionEngine(initialModels);
|
|
870
2733
|
// 第二重(通信层):向客户端下发 RSA 公钥,用于加密敏感参数(如模型 API Key)传输
|
|
871
2734
|
app.get('/api/security/public-key', (_req, res) => {
|
|
872
2735
|
res.json({ ok: true, publicKey: rsaKeys.publicKey, algorithm: 'RSA-OAEP-2048-SHA256' });
|