feihong-code 0.2.2 → 0.5.1
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/FUNDING.yml +4 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -37
- package/.github/ISSUE_TEMPLATE/config.yml +14 -14
- package/.github/ISSUE_TEMPLATE/feature_request.md +28 -28
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -46
- package/.github/SECURITY.md +67 -67
- package/.github/dependabot.yml +16 -0
- package/.github/workflows/ci.yml +1 -1
- package/AGENT-GUIDE.md +382 -237
- package/CHANGELOG.md +405 -73
- package/CODE_OF_CONDUCT.md +56 -56
- package/CONTRIBUTING.md +68 -68
- package/LICENSE +22 -22
- package/README.md +31 -25
- package/README.self-evolve.md +274 -0
- package/dist/agent/code-review.js +2 -0
- package/dist/agent/code-review.js.map +1 -1
- package/dist/agent/code-writer.js +70 -17
- package/dist/agent/code-writer.js.map +1 -1
- package/dist/agent/context-compactor.js +13 -4
- package/dist/agent/context-compactor.js.map +1 -1
- package/dist/agent/experience.js +262 -62
- package/dist/agent/experience.js.map +1 -1
- package/dist/agent/orchestrator.js +148 -68
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/quality-gate.js +10 -4
- package/dist/agent/quality-gate.js.map +1 -1
- package/dist/agent/repo-context.js +181 -0
- package/dist/agent/repo-context.js.map +1 -0
- package/dist/agent/repo-reader.js +92 -99
- package/dist/agent/repo-reader.js.map +1 -1
- package/dist/agent/self-heal.js +38 -48
- package/dist/agent/self-heal.js.map +1 -1
- package/dist/agent/self-improver.js +65 -61
- package/dist/agent/self-improver.js.map +1 -1
- package/dist/agent/subagent-summary.js +31 -0
- package/dist/agent/subagent-summary.js.map +1 -0
- package/dist/agent/subagent.js +52 -2
- package/dist/agent/subagent.js.map +1 -1
- package/dist/agent/symbol-index.js +160 -0
- package/dist/agent/symbol-index.js.map +1 -0
- package/dist/agent/team.js +193 -0
- package/dist/agent/team.js.map +1 -0
- package/dist/cli/commands.js +124 -143
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/index.js +113 -106
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/repl.js +82 -7
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.js +601 -95
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tui.js +145 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/version.js +1 -1
- package/dist/enterprise/audit.js +75 -12
- package/dist/enterprise/audit.js.map +1 -1
- package/dist/enterprise/index.js +14 -11
- package/dist/enterprise/index.js.map +1 -1
- package/dist/enterprise/policy.js +22 -10
- package/dist/enterprise/policy.js.map +1 -1
- package/dist/harness/executor.js +127 -0
- package/dist/harness/executor.js.map +1 -0
- package/dist/harness/harness.js +87 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/index.js +31 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/loader.js +138 -0
- package/dist/harness/loader.js.map +1 -0
- package/dist/harness/reporter.js +34 -0
- package/dist/harness/reporter.js.map +1 -0
- package/dist/harness/types.js +10 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/verifier.js +48 -0
- package/dist/harness/verifier.js.map +1 -0
- package/dist/hello.js +14 -0
- package/dist/hello.js.map +1 -0
- package/dist/memory/auto-summarize.js +208 -0
- package/dist/memory/auto-summarize.js.map +1 -0
- package/dist/memory/index.js +228 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/models/model-router.js +23 -10
- package/dist/models/model-router.js.map +1 -1
- package/dist/plugins/plugin-loader.js +179 -0
- package/dist/plugins/plugin-loader.js.map +1 -0
- package/dist/runtime/event-log.js.map +1 -1
- package/dist/runtime/hooks.js +80 -0
- package/dist/runtime/hooks.js.map +1 -0
- package/dist/self-evolve/hook.js +60 -0
- package/dist/self-evolve/hook.js.map +1 -0
- package/dist/shared/config.js +35 -3
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/i18n.js +535 -0
- package/dist/shared/i18n.js.map +1 -0
- package/dist/skills/grill.js +2 -1
- package/dist/skills/grill.js.map +1 -1
- package/dist/skills/self-heal.js +73 -0
- package/dist/skills/self-heal.js.map +1 -0
- package/dist/skills/skill-loader.js +131 -0
- package/dist/skills/skill-loader.js.map +1 -0
- package/dist/skills/skill-market.js +195 -0
- package/dist/skills/skill-market.js.map +1 -0
- package/dist/tools/analysis/code-analyzer.js +45 -18
- package/dist/tools/analysis/code-analyzer.js.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp/index.js +84 -0
- package/dist/tools/mcp/index.js.map +1 -0
- package/dist/tools/mcp/mcp-client.js +172 -0
- package/dist/tools/mcp/mcp-client.js.map +1 -0
- package/dist/tools/sandbox.js +126 -0
- package/dist/tools/sandbox.js.map +1 -0
- package/dist/tools/shell/exec.js +25 -0
- package/dist/tools/shell/exec.js.map +1 -1
- package/dist/tools/shell/run-shell.tool.js +4 -1
- package/dist/tools/shell/run-shell.tool.js.map +1 -1
- package/dist/tools/skills/load-skill.tool.js +36 -0
- package/dist/tools/skills/load-skill.tool.js.map +1 -0
- package/dist/tools/tool.interface.js.map +1 -1
- package/dist/tools/tool.registry.js +37 -1
- package/dist/tools/tool.registry.js.map +1 -1
- package/dist/tools/web/web.tool.js +139 -0
- package/dist/tools/web/web.tool.js.map +1 -0
- package/dist/web/auth.js +42 -3
- package/dist/web/auth.js.map +1 -1
- package/dist/web/channels.js +171 -0
- package/dist/web/channels.js.map +1 -0
- package/dist/web/public/index.html +3253 -39
- package/dist/web/public/index.html.tmp +3117 -0
- package/dist/web/public/index_new.html +3196 -0
- package/dist/web/server.js +790 -11
- package/dist/web/server.js.map +1 -1
- package/dist/web/task-queue.js +352 -0
- package/dist/web/task-queue.js.map +1 -0
- package/dist/web/web-config.js +143 -0
- package/dist/web/web-config.js.map +1 -0
- package/docs/Deployment_Guide_EN.md +288 -0
- package/docs/Technical_Manual_EN.md +216 -0
- package/docs/User_Manual_EN.md +314 -0
- package/docs/self-evolve-implementation.md +165 -0
- package/docs/self-evolve.md +166 -0
- package/docs//344/272/247/345/223/201/345/274/200/345/217/221/346/226/207/346/241/243.md +614 -614
- package/docs//344/274/201/344/270/232/351/203/250/347/275/262/344/270/216/345/220/210/350/247/204.md +267 -267
- package/docs//344/275/277/347/224/250/350/257/264/346/230/216/344/271/246.md +318 -358
- package/docs//345/270/270/350/247/201/351/227/256/351/242/230/344/270/216/346/225/205/351/232/234/346/216/222/346/237/245.md +130 -130
- package/docs//346/212/200/346/234/257/350/257/264/346/230/216/344/271/246.md +216 -303
- package/docs//346/236/266/346/236/204/344/270/216API.md +238 -238
- package/docs//347/224/250/346/210/267/346/211/213/345/206/214.md +196 -196
- package/docs//351/203/250/347/275/262/346/214/207/345/215/227.md +165 -165
- package/docs//351/203/250/347/275/262/350/257/264/346/230/216/344/271/246.md +288 -0
- package/docs//351/205/215/347/275/256/345/217/202/350/200/203.md +127 -127
- package/package.json +114 -109
- package/tool-schema.json +127 -127
package/dist/web/server.js
CHANGED
|
@@ -8,35 +8,56 @@ exports.startWebServer = startWebServer;
|
|
|
8
8
|
* 飞虹 Code (Muse Code 参照复刻)
|
|
9
9
|
* 晋江市飞虹智科技企业管理有限公司 · 飞扬企源研发中心 · 负责人:吴赐虹
|
|
10
10
|
*
|
|
11
|
-
* M5 Web 控制台:自包含 Express
|
|
12
|
-
*
|
|
11
|
+
* M5 Web 控制台:自包含 Express 服务骨架 + 多视图 API。
|
|
12
|
+
* - 任务队列(P4-1 / P5-2 / P6-4)
|
|
13
|
+
* - 技能市场(插件市场):聚合 ClawHub + Agent-Foundry
|
|
14
|
+
* - 自动化:快捷指令集(一键发起任务)
|
|
15
|
+
* - 模板库:内置 + 用户自定义
|
|
16
|
+
* - 办公助理:文档处理能力清单
|
|
17
|
+
* - 登录:手机号直登(无短信验证,本地会话令牌)
|
|
18
|
+
* - 文件/工作区:右侧任务详情可直接打开文件夹、浏览器、预览文件
|
|
19
|
+
* 鉴权:Bearer Token(fail-closed),主令牌(FH_WEB_TOKEN)与会话令牌并行。
|
|
13
20
|
*/
|
|
14
21
|
const express_1 = __importDefault(require("express"));
|
|
15
22
|
const crypto_1 = require("crypto");
|
|
16
23
|
const path_1 = require("path");
|
|
24
|
+
const fs_1 = require("fs");
|
|
25
|
+
const child_process_1 = require("child_process");
|
|
17
26
|
const auth_1 = require("./auth");
|
|
27
|
+
const task_queue_1 = require("./task-queue");
|
|
18
28
|
const version_1 = require("../cli/version");
|
|
29
|
+
const i18n_1 = require("../shared/i18n");
|
|
19
30
|
const enterprise_1 = require("../enterprise");
|
|
31
|
+
const config_1 = require("../shared/config");
|
|
32
|
+
const memory_1 = require("../memory");
|
|
33
|
+
const auto_summarize_1 = require("../memory/auto-summarize");
|
|
20
34
|
/**
|
|
21
35
|
* 启动 Web 控制台。
|
|
22
36
|
* - 端口:opts.port > FH_WEB_PORT > 8080
|
|
23
37
|
* - 令牌:opts.token > FH_WEB_TOKEN > 自动生成(仅本次会话有效,fail-closed)
|
|
24
38
|
*/
|
|
25
39
|
function startWebServer(opts = {}) {
|
|
40
|
+
// Web 环境无法进行交互式审批,禁用审批检查
|
|
41
|
+
if (process.env.FH_REQUIRE_APPROVAL === undefined) {
|
|
42
|
+
process.env.FH_REQUIRE_APPROVAL = 'false';
|
|
43
|
+
}
|
|
26
44
|
const port = opts.port ?? Number(process.env.FH_WEB_PORT ?? 8080);
|
|
27
45
|
let token = opts.token ?? process.env.FH_WEB_TOKEN ?? '';
|
|
28
46
|
if (!token) {
|
|
29
47
|
token = (0, crypto_1.randomBytes)(24).toString('hex');
|
|
30
|
-
console.log(
|
|
48
|
+
console.log((0, i18n_1.t)('serve.tokenAuto', { token }));
|
|
31
49
|
}
|
|
32
50
|
const app = (0, express_1.default)();
|
|
33
|
-
//
|
|
34
|
-
app.use(express_1.default.json({ limit: '
|
|
35
|
-
//
|
|
36
|
-
const publicDir = (0, path_1.join)(__dirname, 'public');
|
|
37
|
-
app.use(express_1.default.static(publicDir));
|
|
38
|
-
//
|
|
39
|
-
|
|
51
|
+
// 放宽请求体上限以支持截图/图片 base64 上传(8MB),仍可有效防御内存耗尽
|
|
52
|
+
app.use(express_1.default.json({ limit: '8mb' }));
|
|
53
|
+
// 静态仪表盘(开发时直接从 src 读取,避免 dist 被锁)
|
|
54
|
+
const publicDir = process.env.FH_WEB_SRC_PUBLIC || (0, path_1.join)(__dirname, 'public');
|
|
55
|
+
app.use(express_1.default.static(publicDir, { maxAge: '0' }));
|
|
56
|
+
// 登录会话存储(进程内)
|
|
57
|
+
const sessions = new auth_1.SessionStore();
|
|
58
|
+
// 当前 Web 控制台工作区,任务提交缺省时使用
|
|
59
|
+
let serverWorkspaceDir = (0, path_1.resolve)(process.cwd());
|
|
60
|
+
// 公开健康检查(仅暴露版本/状态等观测信息,无敏感数据)
|
|
40
61
|
app.get('/api/health', (_req, res) => {
|
|
41
62
|
res.json({
|
|
42
63
|
ok: true,
|
|
@@ -44,11 +65,738 @@ function startWebServer(opts = {}) {
|
|
|
44
65
|
version: version_1.VERSION,
|
|
45
66
|
signature: version_1.SIGNATURE,
|
|
46
67
|
enterprise: (0, enterprise_1.isEnterpriseEnabled)(),
|
|
68
|
+
lang: (0, i18n_1.getLang)(),
|
|
47
69
|
time: new Date().toISOString(),
|
|
48
70
|
});
|
|
49
71
|
});
|
|
72
|
+
// 手机号直登:无短信验证,生成本地会话令牌
|
|
73
|
+
app.post('/api/auth/login', (req, res) => {
|
|
74
|
+
const body = (req.body ?? {});
|
|
75
|
+
const phone = typeof body?.phone === 'string' ? body.phone.trim() : '';
|
|
76
|
+
if (!phone) {
|
|
77
|
+
res.status(400).json({ ok: false, error: '请输入手机号码' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const sessionToken = sessions.create(phone);
|
|
81
|
+
res.json({ ok: true, token: sessionToken, phone });
|
|
82
|
+
});
|
|
83
|
+
// 任务队列(进程内;服务端静默执行)
|
|
84
|
+
const persistDir = process.env.FH_TASK_PERSIST_DIR?.trim() ||
|
|
85
|
+
(0, path_1.join)(process.env.FH_HOME?.trim() || (0, path_1.join)(require('os').homedir(), '.feihong-code'), 'tasks');
|
|
86
|
+
const queue = new task_queue_1.TaskQueue({
|
|
87
|
+
concurrency: Number(process.env.FH_TASK_CONCURRENCY ?? 2),
|
|
88
|
+
webhookUrl: process.env.FH_TASK_WEBHOOK_URL,
|
|
89
|
+
persistDir,
|
|
90
|
+
});
|
|
91
|
+
// 鉴权:其余 /api 需 Bearer token(静态资源与 login/health 除外)
|
|
92
|
+
// 公开 API(无需认证)
|
|
93
|
+
app.get('/api/drives', (_req, res) => {
|
|
94
|
+
res.json({ ok: true, drives: getAvailableDrives() });
|
|
95
|
+
});
|
|
96
|
+
// 记忆管理 API(公开访问,无敏感信息)
|
|
97
|
+
const memoryConfig = (0, memory_1.getMemoryConfig)();
|
|
98
|
+
app.get('/api/memory/short', (_req, res) => {
|
|
99
|
+
const date = typeof _req.query.date === 'string' ? _req.query.date : undefined;
|
|
100
|
+
const content = (0, memory_1.readShortTerm)(memoryConfig, date ? new Date(date) : undefined);
|
|
101
|
+
res.json({ ok: true, content, date: date || new Date().toISOString().split('T')[0] });
|
|
102
|
+
});
|
|
103
|
+
app.get('/api/memory/long', (_req, res) => {
|
|
104
|
+
const content = (0, memory_1.readLongTerm)(memoryConfig);
|
|
105
|
+
res.json({ ok: true, content });
|
|
106
|
+
});
|
|
107
|
+
app.get('/api/memory/stats', (_req, res) => {
|
|
108
|
+
const stats = (0, memory_1.getMemoryStats)(memoryConfig);
|
|
109
|
+
res.json({ ok: true, ...stats });
|
|
110
|
+
});
|
|
111
|
+
app.post('/api/memory/summarize', async (req, res) => {
|
|
112
|
+
const body = req.body;
|
|
113
|
+
const forceDate = typeof body?.date === 'string' ? body.date : undefined;
|
|
114
|
+
try {
|
|
115
|
+
const result = await (0, auto_summarize_1.summarizeMemory)(memoryConfig, forceDate);
|
|
116
|
+
res.json(result);
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
app.get('/api/memory/history', (_req, res) => {
|
|
123
|
+
const limit = typeof _req.query.limit === 'string' ? parseInt(_req.query.limit) || 30 : 30;
|
|
124
|
+
const history = (0, auto_summarize_1.getSummaryHistory)(memoryConfig, limit);
|
|
125
|
+
res.json({ ok: true, history });
|
|
126
|
+
});
|
|
127
|
+
app.use('/api', (0, auth_1.requireToken)(token, sessions));
|
|
128
|
+
app.get('/api/auth/me', (req, res) => {
|
|
129
|
+
const session = req.user;
|
|
130
|
+
res.json({ ok: true, phone: session?.phone ?? null });
|
|
131
|
+
});
|
|
132
|
+
app.post('/api/tasks', (req, res) => {
|
|
133
|
+
const body = req.body;
|
|
134
|
+
const goal = typeof body?.goal === 'string' ? body.goal.trim() : '';
|
|
135
|
+
if (!goal) {
|
|
136
|
+
res.status(400).json({ ok: false, error: '缺少 goal 字段' });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const agentType = typeof body?.agentType === 'string' ? body.agentType : undefined;
|
|
140
|
+
const permissions = typeof body?.permissions === 'object' && body?.permissions !== null
|
|
141
|
+
? body.permissions
|
|
142
|
+
: undefined;
|
|
143
|
+
const workspaceDir = typeof body?.workspaceDir === 'string' && body.workspaceDir.trim()
|
|
144
|
+
? body.workspaceDir.trim()
|
|
145
|
+
: serverWorkspaceDir;
|
|
146
|
+
const modelId = typeof body?.modelId === 'string' && body.modelId.trim() ? body.modelId.trim() : undefined;
|
|
147
|
+
const record = queue.submit(goal, { modelId, workspaceDir, agentType, permissions });
|
|
148
|
+
res.status(201).json({ ok: true, task: publicTask(record, true) });
|
|
149
|
+
});
|
|
150
|
+
app.get('/api/tasks', (_req, res) => {
|
|
151
|
+
res.json({ ok: true, tasks: queue.list().map((t) => publicTask(t, false)) });
|
|
152
|
+
});
|
|
153
|
+
app.get('/api/tasks/:id', (req, res) => {
|
|
154
|
+
const record = queue.get(req.params.id);
|
|
155
|
+
if (!record) {
|
|
156
|
+
res.status(404).json({ ok: false, error: '任务不存在' });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
res.json({ ok: true, task: publicTask(record, true) });
|
|
160
|
+
});
|
|
161
|
+
// 多轮续接:向当前任务追加一条用户消息,所有对话归属同一任务生命周期
|
|
162
|
+
app.post('/api/tasks/:id/messages', (req, res) => {
|
|
163
|
+
const body = req.body;
|
|
164
|
+
const message = typeof body?.message === 'string' ? body.message.trim() : '';
|
|
165
|
+
if (!message) {
|
|
166
|
+
res.status(400).json({ ok: false, error: '缺少 message 字段' });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const record = queue.continueTask(req.params.id, message);
|
|
170
|
+
if (!record) {
|
|
171
|
+
res.status(409).json({ ok: false, error: '任务不存在或正在执行中,请等待完成后再继续对话' });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
res.status(201).json({ ok: true, task: publicTask(record, true) });
|
|
175
|
+
});
|
|
176
|
+
app.delete('/api/tasks/:id', (req, res) => {
|
|
177
|
+
const success = queue.delete(req.params.id);
|
|
178
|
+
if (!success) {
|
|
179
|
+
res.status(400).json({ ok: false, error: '无法删除运行中的任务' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
res.json({ ok: true });
|
|
183
|
+
});
|
|
184
|
+
// P5-2:webhook 注册/查询
|
|
185
|
+
app.post('/api/webhook', (req, res) => {
|
|
186
|
+
const body = req.body;
|
|
187
|
+
const url = typeof body?.url === 'string' ? body.url.trim() : '';
|
|
188
|
+
if (!url) {
|
|
189
|
+
res.status(400).json({ ok: false, error: '缺少 url 字段' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
queue.setWebhookUrl(url);
|
|
193
|
+
res.json({ ok: true, webhookUrl: url });
|
|
194
|
+
});
|
|
195
|
+
app.get('/api/webhook', (_req, res) => {
|
|
196
|
+
res.json({ ok: true, webhookUrl: queue.getWebhookUrl() || null });
|
|
197
|
+
});
|
|
198
|
+
/* ========== 持久化辅助 ========== */
|
|
199
|
+
const homeDir = (0, config_1.resolveHomeDir)();
|
|
200
|
+
function loadJsonFile(file, fallback) {
|
|
201
|
+
try {
|
|
202
|
+
if (!(0, fs_1.existsSync)(file))
|
|
203
|
+
return fallback;
|
|
204
|
+
return JSON.parse((0, fs_1.readFileSync)(file, 'utf8'));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return fallback;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function saveJsonFile(file, data) {
|
|
211
|
+
try {
|
|
212
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(file), { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* ignore */
|
|
216
|
+
}
|
|
217
|
+
const tmp = file + '.tmp';
|
|
218
|
+
(0, fs_1.writeFileSync)(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
219
|
+
// 原子替换,避免并发读写损坏
|
|
220
|
+
(0, fs_1.writeFileSync)(file, JSON.stringify(data, null, 2), 'utf8');
|
|
221
|
+
try {
|
|
222
|
+
require('fs').unlinkSync(tmp);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/* ignore */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/* ========== 路径安全 ========== */
|
|
229
|
+
// 获取系统所有可用的 Windows 驱动器列表(同步,使用 fs.existsSync 检测)
|
|
230
|
+
function getAvailableDrives() {
|
|
231
|
+
if (process.platform !== 'win32')
|
|
232
|
+
return ['/'];
|
|
233
|
+
const drives = [];
|
|
234
|
+
for (let code = 65; code <= 90; code++) {
|
|
235
|
+
const drive = String.fromCharCode(code) + ':\\';
|
|
236
|
+
if ((0, fs_1.existsSync)(drive))
|
|
237
|
+
drives.push(drive);
|
|
238
|
+
}
|
|
239
|
+
return drives.length > 0 ? drives : [];
|
|
240
|
+
}
|
|
241
|
+
const allowedRoots = () => {
|
|
242
|
+
const roots = [
|
|
243
|
+
(0, path_1.resolve)(serverWorkspaceDir),
|
|
244
|
+
(0, path_1.resolve)(homeDir),
|
|
245
|
+
(0, path_1.resolve)(process.cwd()),
|
|
246
|
+
];
|
|
247
|
+
// 在 Windows 上,动态检测所有可用的本地驱动器根目录
|
|
248
|
+
if (process.platform === 'win32') {
|
|
249
|
+
try {
|
|
250
|
+
const child = (0, child_process_1.spawn)('cmd', ['/c', 'wmic logicaldisk get device'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
251
|
+
let output = '';
|
|
252
|
+
child.stdout.on('data', (d) => { output += d.toString(); });
|
|
253
|
+
child.on('close', () => {
|
|
254
|
+
output.split('\n').forEach((line) => {
|
|
255
|
+
const m = line.trim().match(/^([A-Z])[:\\]$/);
|
|
256
|
+
if (m)
|
|
257
|
+
roots.push(m[1] + ':\\');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch { }
|
|
262
|
+
// 兜底:至少包含 A-Z
|
|
263
|
+
for (let code = 65; code <= 90; code++) {
|
|
264
|
+
const drive = String.fromCharCode(code) + ':\\';
|
|
265
|
+
if (!roots.includes(drive))
|
|
266
|
+
roots.push(drive);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return roots;
|
|
270
|
+
};
|
|
271
|
+
function isPathAllowed(target) {
|
|
272
|
+
const resolved = (0, path_1.resolve)(target);
|
|
273
|
+
for (const root of allowedRoots()) {
|
|
274
|
+
const rel = (0, path_1.relative)(root, resolved);
|
|
275
|
+
if (rel === '' || (!rel.startsWith('..') && !(0, path_1.isAbsolute)(rel))) {
|
|
276
|
+
return (0, fs_1.existsSync)(resolved);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
function assertPathAllowed(target, res) {
|
|
282
|
+
if (!isPathAllowed(target)) {
|
|
283
|
+
res.status(403).json({ ok: false, error: '路径不在允许范围内或不存在' });
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
/* ========== 工作区与文件浏览 ========== */
|
|
289
|
+
app.get('/api/workspace', (_req, res) => {
|
|
290
|
+
res.json({ ok: true, cwd: serverWorkspaceDir });
|
|
291
|
+
});
|
|
292
|
+
app.post('/api/workspace', (req, res) => {
|
|
293
|
+
const body = (req.body ?? {});
|
|
294
|
+
const cwd = typeof body?.cwd === 'string' ? body.cwd.trim() : '';
|
|
295
|
+
if (!cwd) {
|
|
296
|
+
res.status(400).json({ ok: false, error: '缺少 cwd 字段' });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const resolved = (0, path_1.resolve)(cwd);
|
|
300
|
+
if (!(0, fs_1.existsSync)(resolved) || !(0, fs_1.statSync)(resolved).isDirectory()) {
|
|
301
|
+
res.status(400).json({ ok: false, error: '目录不存在' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
serverWorkspaceDir = resolved;
|
|
305
|
+
res.json({ ok: true, cwd: serverWorkspaceDir });
|
|
306
|
+
});
|
|
307
|
+
app.get('/api/workspace/list', (req, res) => {
|
|
308
|
+
const rawPath = typeof req.query.path === 'string' ? req.query.path.trim() : serverWorkspaceDir;
|
|
309
|
+
const dir = (0, path_1.resolve)(rawPath);
|
|
310
|
+
if (!assertPathAllowed(dir, res))
|
|
311
|
+
return;
|
|
312
|
+
try {
|
|
313
|
+
const names = (0, fs_1.readdirSync)(dir);
|
|
314
|
+
const entries = names
|
|
315
|
+
.map((name) => {
|
|
316
|
+
const full = (0, path_1.join)(dir, name);
|
|
317
|
+
try {
|
|
318
|
+
const st = (0, fs_1.lstatSync)(full);
|
|
319
|
+
return {
|
|
320
|
+
name,
|
|
321
|
+
path: full,
|
|
322
|
+
type: st.isDirectory() ? 'dir' : st.isFile() ? 'file' : 'other',
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
.filter(Boolean);
|
|
330
|
+
res.json({ ok: true, cwd: dir, entries });
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
res.status(500).json({ ok: false, error: '读取目录失败: ' + e.message });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
app.post('/api/files/read', (req, res) => {
|
|
337
|
+
const body = (req.body ?? {});
|
|
338
|
+
const file = typeof body?.path === 'string' ? body.path.trim() : '';
|
|
339
|
+
if (!file || !assertPathAllowed(file, res))
|
|
340
|
+
return;
|
|
341
|
+
try {
|
|
342
|
+
const st = (0, fs_1.statSync)(file);
|
|
343
|
+
if (!st.isFile()) {
|
|
344
|
+
res.status(400).json({ ok: false, error: '不是文件' });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (st.size > 2 * 1024 * 1024) {
|
|
348
|
+
res.status(400).json({ ok: false, error: '文件超过 2MB,建议用本地编辑器打开' });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const content = (0, fs_1.readFileSync)(file, 'utf8');
|
|
352
|
+
res.json({ ok: true, path: file, content });
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
res.status(500).json({ ok: false, error: '读取失败: ' + e.message });
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
/* ========== 打开本地文件夹/浏览器 ========== */
|
|
359
|
+
app.post('/api/open/folder', (req, res) => {
|
|
360
|
+
const body = (req.body ?? {});
|
|
361
|
+
const dir = typeof body?.path === 'string' ? body.path.trim() : serverWorkspaceDir;
|
|
362
|
+
if (!dir || !assertPathAllowed(dir, res))
|
|
363
|
+
return;
|
|
364
|
+
try {
|
|
365
|
+
openFolder(dir);
|
|
366
|
+
res.json({ ok: true });
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
res.status(500).json({ ok: false, error: '打开失败: ' + e.message });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
app.post('/api/open/browser', (req, res) => {
|
|
373
|
+
const body = (req.body ?? {});
|
|
374
|
+
const url = typeof body?.url === 'string' ? body.url.trim() : '';
|
|
375
|
+
if (!url) {
|
|
376
|
+
res.status(400).json({ ok: false, error: '缺少 url 字段' });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
openBrowser(url);
|
|
381
|
+
res.json({ ok: true });
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
res.status(500).json({ ok: false, error: '打开失败: ' + e.message });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
/* ========== 上传文件/图片 ========== */
|
|
388
|
+
const uploadsDir = () => (0, path_1.join)(homeDir, 'uploads');
|
|
389
|
+
app.post('/api/upload', (req, res) => {
|
|
390
|
+
const body = (req.body ?? {});
|
|
391
|
+
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
392
|
+
const mime = typeof body?.mime === 'string' ? body.mime.trim() : 'application/octet-stream';
|
|
393
|
+
const data = typeof body?.dataBase64 === 'string' ? body.dataBase64.trim() : '';
|
|
394
|
+
if (!name || !data) {
|
|
395
|
+
res.status(400).json({ ok: false, error: '缺少 name 或 dataBase64 字段' });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
(0, fs_1.mkdirSync)(uploadsDir(), { recursive: true });
|
|
400
|
+
const safeName = name.replace(/[^a-zA-Z0-9_.\-]/g, '_');
|
|
401
|
+
const dest = (0, path_1.join)(uploadsDir(), `${Date.now()}_${safeName}`);
|
|
402
|
+
(0, fs_1.writeFileSync)(dest, Buffer.from(data, 'base64'));
|
|
403
|
+
res.json({ ok: true, path: dest, name, mime });
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
res.status(500).json({ ok: false, error: '上传失败: ' + e.message });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
/* ========== 技能市场(插件市场):聚合 ClawHub + Agent-Foundry ========== */
|
|
410
|
+
const CLAWHUB_API = 'https://clawhub.ai/api/v1/skills';
|
|
411
|
+
const AGENT_FOUNDRY_CATALOG = 'https://raw.githubusercontent.com/hebertzhu/agent-foundry/main/catalog/skills-catalog.json';
|
|
412
|
+
async function fetchWithRetry(url, timeoutMs = 15000, retries = 1) {
|
|
413
|
+
let lastErr;
|
|
414
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
415
|
+
const ctrl = new AbortController();
|
|
416
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
417
|
+
try {
|
|
418
|
+
const r = await fetch(url, {
|
|
419
|
+
signal: ctrl.signal,
|
|
420
|
+
headers: { 'User-Agent': 'fhcode-web/0.5.1' },
|
|
421
|
+
});
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
return r;
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
lastErr = e;
|
|
428
|
+
if (attempt < retries)
|
|
429
|
+
await new Promise((res) => setTimeout(res, 500));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
throw lastErr;
|
|
433
|
+
}
|
|
434
|
+
async function fetchClawHubSkills(keyword, limit) {
|
|
435
|
+
try {
|
|
436
|
+
const url = `${CLAWHUB_API}?limit=${limit}${keyword ? `&q=${encodeURIComponent(keyword)}` : ''}`;
|
|
437
|
+
const r = await fetchWithRetry(url);
|
|
438
|
+
if (!r.ok)
|
|
439
|
+
return [];
|
|
440
|
+
const data = (await r.json());
|
|
441
|
+
return (data.items || []).map((it) => ({
|
|
442
|
+
id: `clawhub:${String(it.slug || '')}`,
|
|
443
|
+
name: String(it.displayName || it.slug || ''),
|
|
444
|
+
description: String(it.summary || it.description || ''),
|
|
445
|
+
source: 'clawhub',
|
|
446
|
+
author: it.slug ? String(it.slug).split('/')[0] : undefined,
|
|
447
|
+
category: 'market',
|
|
448
|
+
tags: Array.isArray(it.topics) ? it.topics : [],
|
|
449
|
+
downloads: Number(it.stats?.downloads ?? 0),
|
|
450
|
+
installHint: `npx skills add ${String(it.slug || '')}`,
|
|
451
|
+
homepage: `https://clawhub.ai/${String(it.slug || '')}`,
|
|
452
|
+
rawUrl: `https://clawhub.ai/${String(it.slug || '')}`,
|
|
453
|
+
}));
|
|
454
|
+
}
|
|
455
|
+
catch (e) {
|
|
456
|
+
console.error('[clawhub] fetch failed:', e);
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function fetchAgentFoundrySkills(keyword, limit) {
|
|
461
|
+
try {
|
|
462
|
+
const r = await fetchWithRetry(AGENT_FOUNDRY_CATALOG);
|
|
463
|
+
if (!r.ok)
|
|
464
|
+
return [];
|
|
465
|
+
const data = (await r.json());
|
|
466
|
+
const arr = Array.isArray(data.skills) ? data.skills : [];
|
|
467
|
+
let list = arr.map((it) => {
|
|
468
|
+
const name = String(it.name || '');
|
|
469
|
+
const publicPath = String(it.public_path || '');
|
|
470
|
+
const category = String(it.category || 'market');
|
|
471
|
+
const rawUrl = publicPath
|
|
472
|
+
? `https://raw.githubusercontent.com/hebertzhu/agent-foundry/main/${publicPath}`
|
|
473
|
+
: `https://github.com/hebertzhu/agent-foundry`;
|
|
474
|
+
return {
|
|
475
|
+
id: `agent-foundry:${name}`,
|
|
476
|
+
name,
|
|
477
|
+
description: `Agent-Foundry 技能包 · 分类:${category}${publicPath ? ` · 源:${publicPath}` : ''}`,
|
|
478
|
+
source: 'agent-foundry',
|
|
479
|
+
author: 'hebertzhu',
|
|
480
|
+
category,
|
|
481
|
+
tags: Array.isArray(it.packs) ? it.packs : [category],
|
|
482
|
+
downloads: 0,
|
|
483
|
+
installHint: `npm i -g @agent-foundry/cli && agent-foundry install ${name}`,
|
|
484
|
+
homepage: `https://github.com/hebertzhu/agent-foundry`,
|
|
485
|
+
rawUrl,
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
if (keyword) {
|
|
489
|
+
const kw = keyword.toLowerCase();
|
|
490
|
+
list = list.filter((s) => s.name.toLowerCase().includes(kw) ||
|
|
491
|
+
s.description.toLowerCase().includes(kw) ||
|
|
492
|
+
(s.tags || []).some((t) => t.toLowerCase().includes(kw)));
|
|
493
|
+
}
|
|
494
|
+
return list.slice(0, limit);
|
|
495
|
+
}
|
|
496
|
+
catch (e) {
|
|
497
|
+
console.error('[agent-foundry] fetch failed:', e);
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
app.get('/api/skills/market', async (req, res) => {
|
|
502
|
+
const q = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
503
|
+
const source = typeof req.query.source === 'string' ? req.query.source : 'all';
|
|
504
|
+
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
|
505
|
+
let results = [];
|
|
506
|
+
if (source === 'clawhub' || source === 'all')
|
|
507
|
+
results = results.concat(await fetchClawHubSkills(q, limit));
|
|
508
|
+
if (source === 'agent-foundry' || source === 'all')
|
|
509
|
+
results = results.concat(await fetchAgentFoundrySkills(q, limit));
|
|
510
|
+
results.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
|
511
|
+
res.json({ ok: true, total: results.length, skills: results.slice(0, limit) });
|
|
512
|
+
});
|
|
513
|
+
// 已安装技能持久化
|
|
514
|
+
const installedSkillsFile = (0, path_1.join)(homeDir, 'skills', 'installed.json');
|
|
515
|
+
app.post('/api/skills/install', async (req, res) => {
|
|
516
|
+
const body = req.body;
|
|
517
|
+
if (!body || !body.id || !body.name || !body.source) {
|
|
518
|
+
res.status(400).json({ ok: false, error: '缺少 id / name / source 字段' });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const dir = (0, path_1.join)(homeDir, 'skills', body.id.replace(/[^a-zA-Z0-9_-]/g, '_'));
|
|
523
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
524
|
+
await saveJsonFile((0, path_1.join)(dir, 'meta.json'), { ...body, installedAt: new Date().toISOString() });
|
|
525
|
+
const installed = loadJsonFile(installedSkillsFile, []);
|
|
526
|
+
if (!installed.find((s) => s.id === body.id)) {
|
|
527
|
+
installed.push(body);
|
|
528
|
+
await saveJsonFile(installedSkillsFile, installed);
|
|
529
|
+
}
|
|
530
|
+
res.json({ ok: true, message: `技能「${body.name}」已记录为已安装` });
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
res.status(500).json({ ok: false, error: '安装失败: ' + e.message });
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
app.get('/api/skills/installed', (_req, res) => {
|
|
537
|
+
res.json({ ok: true, skills: loadJsonFile(installedSkillsFile, []) });
|
|
538
|
+
});
|
|
539
|
+
const automationsFile = (0, path_1.join)(homeDir, 'automations.json');
|
|
540
|
+
app.get('/api/automations', (_req, res) => {
|
|
541
|
+
res.json({ ok: true, automations: loadJsonFile(automationsFile, []) });
|
|
542
|
+
});
|
|
543
|
+
app.post('/api/automations', async (req, res) => {
|
|
544
|
+
const body = (req.body ?? {});
|
|
545
|
+
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
546
|
+
const goal = typeof body.goal === 'string' ? body.goal.trim() : '';
|
|
547
|
+
if (!name || !goal) {
|
|
548
|
+
res.status(400).json({ ok: false, error: '缺少 name 或 goal 字段' });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const list = loadJsonFile(automationsFile, []);
|
|
552
|
+
const rule = {
|
|
553
|
+
id: `auto_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 4)}`,
|
|
554
|
+
name,
|
|
555
|
+
goal,
|
|
556
|
+
modelId: typeof body.modelId === 'string' && body.modelId.trim() ? body.modelId.trim() : undefined,
|
|
557
|
+
workspaceDir: typeof body.workspaceDir === 'string' && body.workspaceDir.trim()
|
|
558
|
+
? body.workspaceDir.trim()
|
|
559
|
+
: undefined,
|
|
560
|
+
createdAt: new Date().toISOString(),
|
|
561
|
+
runCount: 0,
|
|
562
|
+
};
|
|
563
|
+
list.push(rule);
|
|
564
|
+
await saveJsonFile(automationsFile, list);
|
|
565
|
+
res.status(201).json({ ok: true, automation: rule });
|
|
566
|
+
});
|
|
567
|
+
// 注:当前 @types/express 版本缺失 delete 方法类型声明,运行时 Express 完全支持,故局部断言
|
|
568
|
+
app.delete('/api/automations/:id', async (req, res) => {
|
|
569
|
+
const list = loadJsonFile(automationsFile, []);
|
|
570
|
+
const next = list.filter((a) => a.id !== req.params.id);
|
|
571
|
+
if (next.length === list.length) {
|
|
572
|
+
res.status(404).json({ ok: false, error: '指令不存在' });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
await saveJsonFile(automationsFile, next);
|
|
576
|
+
res.json({ ok: true });
|
|
577
|
+
});
|
|
578
|
+
app.post('/api/automations/:id/run', async (req, res) => {
|
|
579
|
+
const list = loadJsonFile(automationsFile, []);
|
|
580
|
+
const rule = list.find((a) => a.id === req.params.id);
|
|
581
|
+
if (!rule) {
|
|
582
|
+
res.status(404).json({ ok: false, error: '指令不存在' });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const record = queue.submit(rule.goal, {
|
|
586
|
+
modelId: rule.modelId,
|
|
587
|
+
workspaceDir: rule.workspaceDir,
|
|
588
|
+
});
|
|
589
|
+
rule.runCount += 1;
|
|
590
|
+
rule.lastRunAt = new Date().toISOString();
|
|
591
|
+
await saveJsonFile(automationsFile, list);
|
|
592
|
+
res.status(201).json({ ok: true, task: publicTask(record, true), runCount: rule.runCount });
|
|
593
|
+
});
|
|
594
|
+
const BUILTIN_TEMPLATES = [
|
|
595
|
+
{
|
|
596
|
+
id: 'tpl-code-gen',
|
|
597
|
+
title: '编写新功能',
|
|
598
|
+
category: '代码',
|
|
599
|
+
icon: '🧩',
|
|
600
|
+
goal: '请用 production-ready 的代码实现以下功能:\n\n(请描述功能点、输入输出、边界条件)',
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
id: 'tpl-debug',
|
|
604
|
+
title: '调试 Bug',
|
|
605
|
+
category: '调试',
|
|
606
|
+
icon: '🐛',
|
|
607
|
+
goal: '以下代码出现异常,请定位根因并修复,附回归验证:\n\n(粘贴错误日志或现象)',
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: 'tpl-refactor',
|
|
611
|
+
title: '重构优化',
|
|
612
|
+
category: '代码',
|
|
613
|
+
icon: '♻️',
|
|
614
|
+
goal: '请在不改变外部行为的前提下重构以下模块,提升可读性与性能:\n\n(粘贴代码或文件路径)',
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
id: 'tpl-test',
|
|
618
|
+
title: '生成测试',
|
|
619
|
+
category: '测试',
|
|
620
|
+
icon: '🧪',
|
|
621
|
+
goal: '请为以下代码生成单元测试与边界用例(覆盖率优先):\n\n(粘贴代码或文件路径)',
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: 'tpl-doc',
|
|
625
|
+
title: '撰写文档',
|
|
626
|
+
category: '文档',
|
|
627
|
+
icon: '📝',
|
|
628
|
+
goal: '请撰写一份技术/产品文档,包含背景、方案、步骤与注意事项:\n\n(说明文档主题)',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
id: 'tpl-review',
|
|
632
|
+
title: '代码审查',
|
|
633
|
+
category: '质量',
|
|
634
|
+
icon: '🔍',
|
|
635
|
+
goal: '请对以下代码做静态/设计审查,列出问题与改进建议:\n\n(粘贴代码或文件路径)',
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: 'tpl-explain',
|
|
639
|
+
title: '解释代码',
|
|
640
|
+
category: '文档',
|
|
641
|
+
icon: '💡',
|
|
642
|
+
goal: '请用通俗语言解释以下代码的逻辑与关键点:\n\n(粘贴代码)',
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: 'tpl-perf',
|
|
646
|
+
title: '性能优化',
|
|
647
|
+
category: '质量',
|
|
648
|
+
icon: '⚡',
|
|
649
|
+
goal: '请分析并优化以下代码的性能瓶颈,给出前后对比:\n\n(粘贴代码或描述场景)',
|
|
650
|
+
},
|
|
651
|
+
];
|
|
652
|
+
const templatesFile = (0, path_1.join)(homeDir, 'templates.json');
|
|
653
|
+
app.get('/api/templates', (_req, res) => {
|
|
654
|
+
const user = loadJsonFile(templatesFile, []);
|
|
655
|
+
res.json({ ok: true, builtin: BUILTIN_TEMPLATES, user });
|
|
656
|
+
});
|
|
657
|
+
app.post('/api/templates', async (req, res) => {
|
|
658
|
+
const body = (req.body ?? {});
|
|
659
|
+
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
660
|
+
const goal = typeof body.goal === 'string' ? body.goal.trim() : '';
|
|
661
|
+
if (!title || !goal) {
|
|
662
|
+
res.status(400).json({ ok: false, error: '缺少 title 或 goal 字段' });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const list = loadJsonFile(templatesFile, []);
|
|
666
|
+
const tpl = {
|
|
667
|
+
id: `tpl_${Date.now()}_${(0, crypto_1.randomUUID)().slice(0, 4)}`,
|
|
668
|
+
title,
|
|
669
|
+
category: typeof body.category === 'string' && body.category.trim() ? body.category.trim() : '自定义',
|
|
670
|
+
goal,
|
|
671
|
+
icon: typeof body.icon === 'string' && body.icon.trim() ? body.icon.trim() : '📄',
|
|
672
|
+
builtin: false,
|
|
673
|
+
};
|
|
674
|
+
list.push(tpl);
|
|
675
|
+
await saveJsonFile(templatesFile, list);
|
|
676
|
+
res.status(201).json({ ok: true, template: tpl });
|
|
677
|
+
});
|
|
678
|
+
app.delete('/api/templates/:id', async (req, res) => {
|
|
679
|
+
const list = loadJsonFile(templatesFile, []);
|
|
680
|
+
const next = list.filter((t) => t.id !== req.params.id);
|
|
681
|
+
if (next.length === list.length) {
|
|
682
|
+
res.status(404).json({ ok: false, error: '模板不存在或不可删除' });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
await saveJsonFile(templatesFile, next);
|
|
686
|
+
res.json({ ok: true });
|
|
687
|
+
});
|
|
688
|
+
/* ========== 办公助理:内置能力清单 ========== */
|
|
689
|
+
app.get('/api/office/capabilities', (_req, res) => {
|
|
690
|
+
res.json({
|
|
691
|
+
ok: true,
|
|
692
|
+
capabilities: [
|
|
693
|
+
{
|
|
694
|
+
id: 'doc-summary',
|
|
695
|
+
icon: '📄',
|
|
696
|
+
title: '文档摘要',
|
|
697
|
+
desc: '粘贴长文档,生成结构化摘要与要点',
|
|
698
|
+
prompt: '请对以下文档做摘要,提取 3-5 个核心要点并列出待办:\n\n',
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
id: 'doc-translate',
|
|
702
|
+
icon: '🌐',
|
|
703
|
+
title: '中英互译',
|
|
704
|
+
desc: '技术文档/代码注释翻译',
|
|
705
|
+
prompt: '请将以下内容准确翻译(保留代码与术语):\n\n',
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
id: 'doc-rewrite',
|
|
709
|
+
icon: '✍️',
|
|
710
|
+
title: '润色改写',
|
|
711
|
+
desc: '把草稿改写为正式/简洁风格',
|
|
712
|
+
prompt: '请润色以下文本,使其更专业简洁:\n\n',
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
id: 'meeting-notes',
|
|
716
|
+
icon: '📋',
|
|
717
|
+
title: '会议纪要',
|
|
718
|
+
desc: '从聊天/录音转写生成行动项',
|
|
719
|
+
prompt: '请从以下记录中提取:决策、负责人、截止时间、待办:\n\n',
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
id: 'email-draft',
|
|
723
|
+
icon: '✉️',
|
|
724
|
+
title: '邮件起草',
|
|
725
|
+
desc: '按要点生成工作邮件',
|
|
726
|
+
prompt: '请起草一封邮件,主题/背景/诉求如下:\n\n',
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
id: 'excel-formula',
|
|
730
|
+
icon: '📊',
|
|
731
|
+
title: '表格公式',
|
|
732
|
+
desc: '描述需求生成 Excel/Sheets 公式',
|
|
733
|
+
prompt: '请写出实现以下需求的表格公式并解释:\n\n',
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
/* ========== 大模型配置 ========== */
|
|
739
|
+
const modelsFile = (0, path_1.join)(homeDir, 'models.json');
|
|
740
|
+
function loadModels() {
|
|
741
|
+
const list = loadJsonFile(modelsFile, []);
|
|
742
|
+
return Array.isArray(list) ? list : [];
|
|
743
|
+
}
|
|
744
|
+
function saveModels(list) {
|
|
745
|
+
saveJsonFile(modelsFile, list);
|
|
746
|
+
// 同步更新任务队列的模型配置,使后续任务使用最新配置
|
|
747
|
+
queue.setModelProviders(list.map(m => ({ id: m.id, type: 'openai-compatible', baseURL: m.apiBase, apiKey: m.apiKey, model: m.name })));
|
|
748
|
+
}
|
|
749
|
+
// 初始化模型提供列表,并注册到任务队列
|
|
750
|
+
const initialModels = loadModels();
|
|
751
|
+
queue.setModelProviders(initialModels.map(m => ({ id: m.id, type: 'openai-compatible', baseURL: m.apiBase, apiKey: m.apiKey, model: m.name })));
|
|
752
|
+
app.get('/api/models', (_req, res) => {
|
|
753
|
+
const list = loadModels();
|
|
754
|
+
res.json({ ok: true, models: list, defaultId: (list.find((m) => m.default) || {}).id || null });
|
|
755
|
+
});
|
|
756
|
+
app.post('/api/models', (req, res) => {
|
|
757
|
+
const body = req.body;
|
|
758
|
+
const name = typeof body?.name === 'string' ? body.name.trim() : '';
|
|
759
|
+
const apiBase = typeof body?.apiBase === 'string' ? body.apiBase.trim() : '';
|
|
760
|
+
const apiKey = typeof body?.apiKey === 'string' ? body.apiKey : '';
|
|
761
|
+
const reasoning = typeof body?.reasoning === 'string' ? body.reasoning : '';
|
|
762
|
+
if (!name) {
|
|
763
|
+
res.status(400).json({ ok: false, error: '请填写模型名称' });
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const list = loadModels();
|
|
767
|
+
const id = typeof body?.id === 'string' && body.id.trim() ? body.id.trim() : (0, crypto_1.randomUUID)();
|
|
768
|
+
const idx = list.findIndex((m) => m.id === id);
|
|
769
|
+
const cfg = { id, name, apiBase, apiKey, reasoning };
|
|
770
|
+
if (idx >= 0)
|
|
771
|
+
list[idx] = { ...list[idx], ...cfg };
|
|
772
|
+
else
|
|
773
|
+
list.push(cfg);
|
|
774
|
+
saveModels(list);
|
|
775
|
+
res.json({ ok: true, model: cfg });
|
|
776
|
+
});
|
|
777
|
+
app.delete('/api/models/:id', (req, res) => {
|
|
778
|
+
let list = loadModels();
|
|
779
|
+
const target = list.find((m) => m.id === req.params.id);
|
|
780
|
+
list = list.filter((m) => m.id !== req.params.id);
|
|
781
|
+
// 若删除的是默认,且没有其它默认,则把第一个设为默认
|
|
782
|
+
if (target?.default && !list.some((m) => m.default) && list.length)
|
|
783
|
+
list[0].default = true;
|
|
784
|
+
saveModels(list);
|
|
785
|
+
res.json({ ok: true });
|
|
786
|
+
});
|
|
787
|
+
app.post('/api/models/:id/default', (req, res) => {
|
|
788
|
+
const list = loadModels();
|
|
789
|
+
const target = list.find((m) => m.id === req.params.id);
|
|
790
|
+
if (!target) {
|
|
791
|
+
res.status(404).json({ ok: false, error: '模型不存在' });
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
list.forEach((m) => (m.default = m.id === req.params.id));
|
|
795
|
+
saveModels(list);
|
|
796
|
+
res.json({ ok: true, defaultId: target.id });
|
|
797
|
+
});
|
|
50
798
|
const server = app.listen(port, () => {
|
|
51
|
-
console.log(
|
|
799
|
+
console.log((0, i18n_1.t)('serve.started', { port }));
|
|
52
800
|
});
|
|
53
801
|
return {
|
|
54
802
|
port,
|
|
@@ -57,4 +805,35 @@ function startWebServer(opts = {}) {
|
|
|
57
805
|
close: () => server.close(),
|
|
58
806
|
};
|
|
59
807
|
}
|
|
808
|
+
/** 对外暴露的任务视图。withSteps=true 时携带思维链路步骤与对话历史(仅单任务详情使用),列表接口剔除以降低负载 */
|
|
809
|
+
function publicTask(r, withSteps = false) {
|
|
810
|
+
if (withSteps)
|
|
811
|
+
return r;
|
|
812
|
+
const { steps: _steps, conversation: _conversation, ...rest } = r;
|
|
813
|
+
return rest;
|
|
814
|
+
}
|
|
815
|
+
/** 使用系统默认程序打开本地文件夹 */
|
|
816
|
+
function openFolder(dir) {
|
|
817
|
+
if (process.platform === 'win32') {
|
|
818
|
+
(0, child_process_1.spawn)('explorer', [dir], { detached: true, stdio: 'ignore' }).unref();
|
|
819
|
+
}
|
|
820
|
+
else if (process.platform === 'darwin') {
|
|
821
|
+
(0, child_process_1.spawn)('open', [dir], { detached: true, stdio: 'ignore' }).unref();
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
(0, child_process_1.spawn)('xdg-open', [dir], { detached: true, stdio: 'ignore' }).unref();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/** 使用系统默认浏览器打开 URL */
|
|
828
|
+
function openBrowser(url) {
|
|
829
|
+
if (process.platform === 'win32') {
|
|
830
|
+
(0, child_process_1.spawn)('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
|
|
831
|
+
}
|
|
832
|
+
else if (process.platform === 'darwin') {
|
|
833
|
+
(0, child_process_1.spawn)('open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
(0, child_process_1.spawn)('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
60
839
|
//# sourceMappingURL=server.js.map
|