aims-workstation 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example
CHANGED
|
@@ -9,7 +9,6 @@ LLM_MODEL=def
|
|
|
9
9
|
|
|
10
10
|
# ARM 系统(助手档案 + 经验库):站点地址缺省公司内网 http://arm.agent-platform.dev.aimstek.cn,留空即用默认
|
|
11
11
|
# 匿名可浏览目录;下载/上岗/新建/经验回流需令牌——到 ARM 生成访问令牌(arm_pat_…)后填入,或在「系统设置 · ARM 源」粘贴
|
|
12
|
-
# ARM_MCP_URL=https://your-arm-host/api/mcp # 可选:私有化部署时覆盖
|
|
13
12
|
# ARM_API_BASE=http://localhost:3001 # 可选:本地联调 ARM 时启用
|
|
14
13
|
# PAT 留空则自动读取 ~/.arm/config.json 的 token
|
|
15
14
|
ARM_PAT=
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"@earendil-works/pi-ai": "^1.2.0"
|
|
14
14
|
},
|
|
15
15
|
"packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1",
|
|
16
|
-
"version": "0.1.
|
|
16
|
+
"version": "0.1.9",
|
|
17
17
|
"description": "AIMS 工作站:本地智能体工作站——ARM 助手本地化(下载/离线/升级)、工作台对话式定制、多工作区隔离、分级自主与审计",
|
|
18
18
|
"bin": {
|
|
19
19
|
"aims-workstation": "bin/aims-workstation.js"
|
package/server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Agent 层基于 @earendil-works/pi-ai(统一 LLM API),前端为 web/ 预构建的 shadcn/ui 应用(web/dist)
|
|
4
4
|
'use strict';
|
|
5
5
|
// TLS 证书校验关闭(用户决策:内网私有部署自签证书常见,能访问优先)
|
|
6
|
-
// 覆盖所有出站 fetch(ARM REST
|
|
6
|
+
// 覆盖所有出站 fetch(ARM REST、LLM、面板代理)。本工具面向内网/本机使用。
|
|
7
7
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
8
8
|
const http = require('http');
|
|
9
9
|
const fs = require('fs');
|
|
@@ -76,7 +76,7 @@ const PORT = +(process.env.PORT || env.PORT || 5600);
|
|
|
76
76
|
const HOST = process.env.AIMS_HOST || '0.0.0.0'; // 默认局域网可达(团队共用);仅本机使用可设 AIMS_HOST=127.0.0.1
|
|
77
77
|
// ── 运行时配置(ARM 源与 LLM 均可私有化部署:在「系统设置」页切换,存 AIMS_HOME/config/platform.json,优先级 运行时 > 环境变量 > .env)──
|
|
78
78
|
const CFG_FILE = path.join(AIMS_HOME, 'config/platform.json');
|
|
79
|
-
const cfg = { arm_api_base: '',
|
|
79
|
+
const cfg = { arm_api_base: '', arm_token: '', llm_base_url: '', llm_api_key: '', llm_model: '', llm_context_tokens: 0, scene_profile: '', access_token: '' };
|
|
80
80
|
// 守护栏默认策略(可在「系统设置·守护栏」维护,存 platform.json,保存即热生效):deny=正则源串(命中即永久拒绝,任何级别不豁免);
|
|
81
81
|
// ro=只读白名单命令名(免确认直行);sudo_u_l2=true 时 `sudo -u <用户>` 降权形式不再永久拒绝、按写命令走 L2 人工确认(裸 sudo 仍拒绝)
|
|
82
82
|
const GUARD_DEFAULT = {
|
|
@@ -92,20 +92,23 @@ function normalizeGuard(g) {
|
|
|
92
92
|
sudo_u_l2: !!g.sudo_u_l2,
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
function loadCfg() { try { Object.assign(cfg, JSON.parse(fs.readFileSync(CFG_FILE, 'utf8'))); } catch { } cfg.guard = normalizeGuard(cfg.guard); }
|
|
95
|
+
function loadCfg() { try { Object.assign(cfg, JSON.parse(fs.readFileSync(CFG_FILE, 'utf8'))); } catch { } delete cfg.arm_mcp_url; /* MCP 通道已退役:剥离旧配置残留 */ cfg.guard = normalizeGuard(cfg.guard); }
|
|
96
96
|
loadCfg();
|
|
97
97
|
const guardNow = () => cfg.guard;
|
|
98
98
|
function saveCfg() { fs.writeFileSync(CFG_FILE, JSON.stringify(cfg, null, 2)); }
|
|
99
|
-
// ARM 站点地址:运行时配置 > ARM_API_BASE(env) >
|
|
99
|
+
// ARM 站点地址:运行时配置 > ARM_API_BASE(env) > 公司内网默认(内网可达即开箱可用)
|
|
100
|
+
//(2026-10-21 全面切换新版 ARM:仅 REST 通道,MCP 通道及旧版兼容回落已退役)
|
|
100
101
|
const ARM_DEFAULT_BASE = 'http://arm.agent-platform.dev.aimstek.cn';
|
|
101
|
-
const armApiBase = () => cfg.arm_api_base || process.env.ARM_API_BASE || env.ARM_API_BASE ||
|
|
102
|
-
const armMcpUrl = () => cfg.arm_mcp_url || `${armApiBase()}/api/mcp`;
|
|
102
|
+
const armApiBase = () => cfg.arm_api_base || process.env.ARM_API_BASE || env.ARM_API_BASE || ARM_DEFAULT_BASE;
|
|
103
103
|
const armApiToken = () => cfg.arm_token || env.ARM_API_TOKEN || ARM_PAT;
|
|
104
104
|
// LLM:OpenAI 兼容端点(baseUrl/模型/密钥),缺省公司内网 bifrost 网关(内网可达即可用,无需配置)
|
|
105
105
|
const LLM_DEFAULT_BASE = 'http://bifrost.infra.dev.aimstek.cn/v1';
|
|
106
106
|
const llmBaseUrl = () => cfg.llm_base_url || env.LLM_BASE_URL || LLM_DEFAULT_BASE;
|
|
107
107
|
const llmApiKey = () => cfg.llm_api_key || env.LLM_API_KEY || 'def';
|
|
108
108
|
const llmModelName = () => cfg.llm_model || env.LLM_MODEL || 'def';
|
|
109
|
+
// 上下文窗口(token):设置页可配(跟随所选模型的真实窗口),驱动会话历史裁剪预算;0/未配 = 默认 128K
|
|
110
|
+
const LLM_DEFAULT_CONTEXT_TOKENS = 128_000;
|
|
111
|
+
const llmContextTokens = () => { const n = +cfg.llm_context_tokens; return Number.isFinite(n) && n > 0 ? n : LLM_DEFAULT_CONTEXT_TOKENS; };
|
|
109
112
|
// 场景画像:平台级、全局一份的用户场景描述(团队环境/系统清单/规范),注入所有助手;空则不注入。
|
|
110
113
|
// 与知识检索互补(常驻事实 vs 按需检索);凭据不进画像(放"凭据在哪、找谁要")。
|
|
111
114
|
const sceneProfile = () => String(cfg.scene_profile || '').trim();
|
|
@@ -175,6 +178,8 @@ async function llmProbe() { // 设置页「保存并测试」:一次极小补
|
|
|
175
178
|
}
|
|
176
179
|
async function llmComplete(systemPrompt, userText) { // 无工具的一次性调用(起草/摘要)
|
|
177
180
|
const r = await models.complete(model, { systemPrompt, messages: [{ role: 'user', content: userText, timestamp: Date.now() }], tools: [] });
|
|
181
|
+
// pi-ai 把认证/请求失败表现为 stopReason=error 而非异常(同 llmProbe 判定)——不抛会变成空串静默流向调用方
|
|
182
|
+
if (r.stopReason === 'error') throw new Error(`LLM 请求失败: ${r.errorMessage || r.rawStopReason || 'stop=error'}`);
|
|
178
183
|
return (r.content || []).filter(b => b.type === 'text').map(b => b.text).join('');
|
|
179
184
|
}
|
|
180
185
|
|
|
@@ -186,26 +191,7 @@ const world = { devices: [], alarms: [], kpi: {}, work_orders: [], inspections:
|
|
|
186
191
|
function audit(entry) { entry.ts = new Date().toISOString(); fs.appendFileSync(AUDIT_LOG, JSON.stringify(entry) + '\n'); }
|
|
187
192
|
function metric(entry) { entry.ts = new Date().toISOString(); fs.appendFileSync(METRICS_LOG, JSON.stringify(entry) + '\n'); }
|
|
188
193
|
|
|
189
|
-
//
|
|
190
|
-
async function mcpCall(name, args, { pat = false, timeout_ms = 20000 } = {}) {
|
|
191
|
-
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' };
|
|
192
|
-
if (pat && ARM_PAT) headers['Authorization'] = `Bearer ${ARM_PAT}`;
|
|
193
|
-
const ctrl = new AbortController();
|
|
194
|
-
const timer = setTimeout(() => ctrl.abort(), timeout_ms); // 离线快速失败(降级本地库)
|
|
195
|
-
let data;
|
|
196
|
-
try {
|
|
197
|
-
const res = await fetch(armMcpUrl(), {
|
|
198
|
-
method: 'POST', headers, signal: ctrl.signal,
|
|
199
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: crypto.randomUUID(), method: 'tools/call', params: { name, arguments: args } }),
|
|
200
|
-
});
|
|
201
|
-
data = await res.json();
|
|
202
|
-
} finally { clearTimeout(timer); }
|
|
203
|
-
if (data.error) throw new Error(`ARM MCP ${name}: ${data.error.message}`);
|
|
204
|
-
if ((data.result || {}).isError) throw new Error(`ARM MCP ${name} 拒绝: ${((data.result).content || []).map(c => c.text).join('').slice(0, 150)}`);
|
|
205
|
-
return ((data.result || {}).content || []).map(c => c.text || '').join('\n');
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ARM REST 客户端(v2 开发版结构化通道;{ ok, data, msg } 包络,ok=false 抛错)
|
|
194
|
+
// ARM REST 客户端(v2 开发版结构化通道;{ ok, data, msg } 包络,ok=false 抛错)——目录/检索/下载/回流全链路唯一通道
|
|
209
195
|
async function armRest(pathname, init = {}, timeout_ms = 20000) {
|
|
210
196
|
const headers = { 'Content-Type': 'application/json', ...(init.headers || {}) };
|
|
211
197
|
const token = armApiToken();
|
|
@@ -232,6 +218,11 @@ async function armRest(pathname, init = {}, timeout_ms = 20000) {
|
|
|
232
218
|
throw e;
|
|
233
219
|
} finally { clearTimeout(timer); }
|
|
234
220
|
}
|
|
221
|
+
// 知识写入(经验回流链):REST v2,令牌走 armApiToken() 三级回落(设置页 > env > ~/.arm PAT)
|
|
222
|
+
// 返回 data: { id, name, shareUrl, … }——id 为 UUID,供指标留痕与回流记录定位
|
|
223
|
+
async function armCreateKnowledge({ name, description, content, tags }) {
|
|
224
|
+
return armRest('/api/v1/knowledges', { method: 'POST', body: JSON.stringify({ name, description, content, tags }) }, 25000);
|
|
225
|
+
}
|
|
235
226
|
const vVer = (v) => (String(v || '').startsWith('v') ? String(v) : `v${v || '0.0.0'}`); // REST 无 v 前缀,统一内部表示
|
|
236
227
|
|
|
237
228
|
const agentCache = new Map();
|
|
@@ -240,30 +231,14 @@ async function getArmAgent(agentId) {
|
|
|
240
231
|
if (lm) return { name: lm.name, version: lm.version, prompt: lm.prompt, knowledges: (lm.knowledges || []).map(k => k.ref) };
|
|
241
232
|
const hit = agentCache.get(agentId);
|
|
242
233
|
if (hit && Date.now() - hit.at < 5 * 60_000) return hit.data;
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const prompt = (text.match(/## Prompt\n([\s\S]*?)(\n## |\n*$)/) || [])[1]?.trim() || '';
|
|
247
|
-
const knowledges = [...text.matchAll(/^- (.+?) v[\d.]+((\w+))/gm)].map(m => m[1]);
|
|
248
|
-
const data = { name, version, prompt, knowledges };
|
|
234
|
+
const d = await armRest(`/api/v1/agents/${agentId}`);
|
|
235
|
+
const knowledges = (d.knowledges || []).map(kb => (kb.knowledge && kb.knowledge.name) || kb.knowledgeId || '').filter(Boolean);
|
|
236
|
+
const data = { name: d.name || agentId, version: vVer(d.version), prompt: d.prompt || '', knowledges };
|
|
249
237
|
agentCache.set(agentId, { data, at: Date.now() });
|
|
250
238
|
return data;
|
|
251
239
|
}
|
|
252
240
|
|
|
253
|
-
//
|
|
254
|
-
function parseAgentCards(raw) {
|
|
255
|
-
const cards = [];
|
|
256
|
-
for (const block of raw.split(/\n(?=## )/)) {
|
|
257
|
-
const m = block.match(/^## (.+?) \((v[\d.]+)\)\nID: ([0-9a-f-]{36})/);
|
|
258
|
-
if (!m) continue;
|
|
259
|
-
const bind = block.match(/绑定:(\d+) Skill \/ (\d+) Knowledge/);
|
|
260
|
-
const desc = (block.split('\n').filter(l => l && !l.startsWith('#') && !l.startsWith('ID:') && !l.startsWith('绑定'))[0] || '').trim();
|
|
261
|
-
cards.push({ name: m[1], version: m[2], agent_id: m[3], skills: +(bind?.[1] || 0), knowledges: +(bind?.[2] || 0), description: desc.slice(0, 120) });
|
|
262
|
-
}
|
|
263
|
-
return cards;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 助手目录:REST 分页结构化优先(可全量),旧版 ARM 回落 MCP markdown 解析(limit 上限 20)
|
|
241
|
+
// 助手目录:REST 分页结构化(可全量)
|
|
267
242
|
async function armListAgents(keyword = '') {
|
|
268
243
|
const out = [];
|
|
269
244
|
for (let page = 1; page <= 5; page++) {
|
|
@@ -274,26 +249,16 @@ async function armListAgents(keyword = '') {
|
|
|
274
249
|
return out.map(a => ({ agent_id: a.id, name: a.name, version: vVer(a.version), skills: a.skillsCount || 0, knowledges: a.knowledgesCount || 0, description: String(a.description || '').slice(0, 120), status: a.status || 'active', created_by: a.creatorName || a.createdBy || '' }));
|
|
275
250
|
}
|
|
276
251
|
|
|
277
|
-
// 知识检索(全局经验库):REST 混合检索(RAG 语义 ⊕
|
|
252
|
+
// 知识检索(全局经验库):REST 混合检索(RAG 语义 ⊕ 关键词);不可达由调用方降级(本地知识关键词命中)
|
|
278
253
|
async function searchKnowledgeItems(query, topK = 5) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
};
|
|
285
|
-
} catch {
|
|
286
|
-
const text = await mcpCall('search_knowledges', { query, limit: topK });
|
|
287
|
-
const items = text.split(/\n(?=## )/).filter(Boolean).map(b => {
|
|
288
|
-
const title = (b.match(/^## (.+?)(?:(相似度 (\d+)%))?/) || [])[1] || b.slice(0, 40);
|
|
289
|
-
return { title: title.trim(), similarity: +((b.match(/相似度 (\d+)%/) || [])[1] || 0) || null, snippet: b.replace(/^## .*\n?/, '').split('[完整内容')[0].slice(0, 320) };
|
|
290
|
-
});
|
|
291
|
-
return { via: 'mcp', items };
|
|
292
|
-
}
|
|
254
|
+
const d = await armRest('/api/v1/knowledges/search', { method: 'POST', body: JSON.stringify({ query, topK }) });
|
|
255
|
+
return {
|
|
256
|
+
via: `rest:${d.mode || 'hybrid'}${d.degraded ? ':degraded' : ''}`,
|
|
257
|
+
items: (d.items || []).map(it => ({ title: it.name, similarity: it.score != null ? Math.round(it.score * 100) : null, snippet: String(it.snippet || '').slice(0, 320), id: it.knowledgeId })),
|
|
258
|
+
};
|
|
293
259
|
}
|
|
294
260
|
|
|
295
261
|
// ── 本地助手库(07-助手本地化:下载物化 → 离线可用;按版本目录存档,升级原子切指针)──
|
|
296
|
-
const armSourceTag = (base) => { try { return `arm://${new URL(base).host}`; } catch { return 'arm://unknown'; } };
|
|
297
262
|
const regFile = () => path.join(ARM_AGENTS_DIR, 'registry.json');
|
|
298
263
|
function loadRegistry() {
|
|
299
264
|
try { return JSON.parse(fs.readFileSync(regFile(), 'utf8')); } catch { return { agents: [], last_sync_at: null }; }
|
|
@@ -319,13 +284,7 @@ function cmpVersion(a, b) { // 'v1.10.0' vs 'v1.9.3' 语义化比较
|
|
|
319
284
|
}
|
|
320
285
|
const slug = (s) => String(s).trim().replace(/[\\/:*?"<>|\s]+/g, '-').slice(0, 60) || 'knowledge';
|
|
321
286
|
|
|
322
|
-
//
|
|
323
|
-
async function downloadAgent(agentId) {
|
|
324
|
-
try { return await downloadAgentRest(agentId); }
|
|
325
|
-
catch { return await downloadAgentMcp(agentId); }
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// 注册表提交(两路下载共用):写版本目录后原子切指针
|
|
287
|
+
// 注册表提交:写版本目录后原子切指针
|
|
329
288
|
function commitDownload(manifest) {
|
|
330
289
|
const reg = loadRegistry();
|
|
331
290
|
let e = reg.agents.find(a => a.agent_id === manifest.agent_id);
|
|
@@ -349,7 +308,8 @@ function uninstallLocalAgent(agentId, purge_workspaces) {
|
|
|
349
308
|
return true;
|
|
350
309
|
}
|
|
351
310
|
|
|
352
|
-
|
|
311
|
+
// 下载物化:REST 结构化详情 + knowledgeId 直取全文
|
|
312
|
+
async function downloadAgent(agentId) {
|
|
353
313
|
const d = await armRest(`/api/v1/agents/${agentId}`);
|
|
354
314
|
const version = vVer(d.version);
|
|
355
315
|
const verDir = path.join(ARM_AGENTS_DIR, agentId, 'versions', version);
|
|
@@ -396,48 +356,9 @@ async function downloadAgentRest(agentId) {
|
|
|
396
356
|
return manifest;
|
|
397
357
|
}
|
|
398
358
|
|
|
399
|
-
// 旧版 ARM 兼容:get_agent markdown 解析 + 知识 ID 经检索定位
|
|
400
|
-
async function downloadAgentMcp(agentId) {
|
|
401
|
-
const text = await mcpCall('get_agent', { id: agentId }, { timeout_ms: 25000 });
|
|
402
|
-
const section = (h) => (text.match(new RegExp(`## ${h}\\n([\\s\\S]*?)(\\n## |\\n*$)`)) || [])[1] || '';
|
|
403
|
-
const parseList = (s) => [...s.matchAll(/^- (.+?) (v[\d.]+)((\w+))/gm)].map(m => ({ ref: m[1].trim(), version: m[2], kind: m[3] }));
|
|
404
|
-
const name = (text.match(/^#\s+(.+?)\s+\(/m) || [])[1] || agentId;
|
|
405
|
-
const version = (text.match(/^#\s+.+?\s+\((v[\d.]+)\)/m) || [])[1] || 'v0.0.0';
|
|
406
|
-
const prompt = (text.match(/## Prompt\n([\s\S]*?)(\n## |\n*$)/) || [])[1]?.trim() || '';
|
|
407
|
-
const description = section('描述').trim();
|
|
408
|
-
const skills = parseList(section('Skills'));
|
|
409
|
-
const verDir = path.join(ARM_AGENTS_DIR, agentId, 'versions', version);
|
|
410
|
-
fs.mkdirSync(path.join(verDir, 'knowledges'), { recursive: true });
|
|
411
|
-
const knowledges = [];
|
|
412
|
-
for (const k of parseList(section('Knowledges'))) {
|
|
413
|
-
let file = null, note = '';
|
|
414
|
-
try { // 知识 ID 经检索定位(ARM 无按名直取接口),再拉全文。注意:知识正文自身含 "## " 内部标题,不可按标题切块——须按「标题→首个 [完整内容] 标记」成对匹配
|
|
415
|
-
const sres = await mcpCall('search_knowledges', { query: k.ref, limit: 5 }, { timeout_ms: 20000 });
|
|
416
|
-
const pairs = [...sres.matchAll(/^## (.+?)(?:(相似度 \d+%))?\n[\s\S]*?\[完整内容: get_knowledge id=([0-9a-f-]{36})\]/gm)];
|
|
417
|
-
const kid = pairs.find(m => m[1].trim() === k.ref)?.[2];
|
|
418
|
-
if (kid) {
|
|
419
|
-
const full = await mcpCall('get_knowledge', { id: kid }, { timeout_ms: 20000 });
|
|
420
|
-
file = `knowledges/${slug(k.ref)}.md`;
|
|
421
|
-
fs.writeFileSync(path.join(verDir, file), full);
|
|
422
|
-
} else note = 'ARM 检索未命中该知识,离线不可用';
|
|
423
|
-
} catch (e) { note = `知识拉取失败: ${String(e.message).slice(0, 80)}`; }
|
|
424
|
-
knowledges.push({ ...k, file, note });
|
|
425
|
-
}
|
|
426
|
-
const manifest = {
|
|
427
|
-
agent_id: agentId, name, version, description,
|
|
428
|
-
downloaded_at: new Date().toISOString(), source: armSourceTag(armMcpUrl()), prompt,
|
|
429
|
-
skills, knowledges,
|
|
430
|
-
};
|
|
431
|
-
fs.writeFileSync(path.join(verDir, 'prompt.md'), prompt);
|
|
432
|
-
fs.writeFileSync(path.join(verDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
433
|
-
commitDownload(manifest);
|
|
434
|
-
audit({ actor: 'arm-library', agentId, action: 'download', via: 'mcp', version, knowledges_local: knowledges.filter(k => k.file).length });
|
|
435
|
-
return manifest;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
359
|
// 同步:比对远端目录与本地库,产出 new/update/delisted 事件;顺带冲账离线经验 outbox
|
|
439
360
|
async function armSync() {
|
|
440
|
-
const remote = await armListAgents();
|
|
361
|
+
const remote = await armListAgents();
|
|
441
362
|
const reg = loadRegistry();
|
|
442
363
|
const remoteIds = new Set(remote.map(r => r.agent_id));
|
|
443
364
|
const events = { new: [], update: [], delisted: [] };
|
|
@@ -465,7 +386,7 @@ async function flushExperienceOutbox() {
|
|
|
465
386
|
const lines = fs.readFileSync(OUTBOX_FILE, 'utf8').split('\n').filter(Boolean);
|
|
466
387
|
const remain = []; let ok = 0;
|
|
467
388
|
for (const line of lines) {
|
|
468
|
-
try { const it = JSON.parse(line); await
|
|
389
|
+
try { const it = JSON.parse(line); await armCreateKnowledge(it.args); ok++; }
|
|
469
390
|
catch { remain.push(line); }
|
|
470
391
|
}
|
|
471
392
|
fs.writeFileSync(OUTBOX_FILE, remain.length ? remain.join('\n') + '\n' : '');
|
|
@@ -687,16 +608,15 @@ const TOOLS = {
|
|
|
687
608
|
const content = ['---', 'kind: experience', 'source: aims-platform-mvp', `agent: ${arm.name || agentId}@${arm.version || ''}`, 'scenario: 立库A区智能运维@mvp', '---', '', markdown].join('\n');
|
|
688
609
|
const args = { name: title, description: `MVP回流:${title}`, content, tags: ['MVP回流'] };
|
|
689
610
|
let r;
|
|
690
|
-
try { r = await
|
|
611
|
+
try { r = await armCreateKnowledge(args); }
|
|
691
612
|
catch (e) { // 离线:入 outbox 缓写,恢复连接后经「检查更新」自动回流
|
|
692
613
|
fs.appendFileSync(OUTBOX_FILE, JSON.stringify({ at: new Date().toISOString(), agentId, args }) + '\n');
|
|
693
614
|
audit({ actor: 'experience-reflow', target: 'outbox', title });
|
|
694
615
|
return `ARM 不可达(${String(e.message).slice(0, 60)}),经验已存本地待回流队列;恢复连接后在助手大厅点「检查更新」将自动回流。`;
|
|
695
616
|
}
|
|
696
|
-
|
|
697
|
-
metric({ kind: 'experience', agentId, title, knowledge_id: kid });
|
|
617
|
+
metric({ kind: 'experience', agentId, title, knowledge_id: r.id });
|
|
698
618
|
audit({ actor: 'experience-reflow', target: 'ARM', title });
|
|
699
|
-
return `经验已沉淀到 ARM
|
|
619
|
+
return `经验已沉淀到 ARM 知识库:${r.name}(id: ${r.id}${r.shareUrl ? `,${r.shareUrl}` : ''})`;
|
|
700
620
|
},
|
|
701
621
|
},
|
|
702
622
|
};
|
|
@@ -1389,6 +1309,26 @@ const DEV_TOOLS = {
|
|
|
1389
1309
|
const devCtx = { agentId: null };
|
|
1390
1310
|
const devSessions = new Map();
|
|
1391
1311
|
|
|
1312
|
+
// 会话上下文裁剪(dev/主对话共用):前端 sessionId 固定(dev-<agentId前缀> / chat-<agentId>-<wsId>),会话跨请求长存——
|
|
1313
|
+
// 不裁剪会无限增长,最终超过模型上下文窗口被网关整单拒绝(2026-10-21 海立实例"发消息没反应"根因)。
|
|
1314
|
+
// 预算随设置页「上下文窗口(token)」联动:预留 16K token 给系统提示/当前轮/输出,1.6 char/token 对中文最坏比例(~0.67 token/char)仍在预留内。
|
|
1315
|
+
const ctxCharBudget = () => Math.max(4_000, Math.floor((llmContextTokens() - 16_000) * 1.6));
|
|
1316
|
+
// 超预算从最旧轮次整轮丢弃;下刀点只选 user 消息边界,剩余前缀不会出现孤儿 toolResult(转换层要求其紧跟 assistant 的 toolCall);当前轮永不裁。
|
|
1317
|
+
function trimContext(ctx, budget = ctxCharBudget()) {
|
|
1318
|
+
const msgs = ctx.messages;
|
|
1319
|
+
const size = () => msgs.reduce((n, m) => n + JSON.stringify(m.content ?? '').length, 0);
|
|
1320
|
+
if (size() <= budget) return 0;
|
|
1321
|
+
const lastUser = msgs.length - 1 - [...msgs].reverse().findIndex((m) => m.role === 'user');
|
|
1322
|
+
let dropped = 0;
|
|
1323
|
+
while (size() > budget) {
|
|
1324
|
+
const cut = msgs.slice(1, lastUser - dropped).findIndex((m) => m.role === 'user');
|
|
1325
|
+
if (cut < 0) break; // 当前轮之前再无完整轮次可丢(单轮就超预算只能放行,交由模型侧报错)
|
|
1326
|
+
dropped += cut + 1;
|
|
1327
|
+
msgs.splice(0, cut + 1);
|
|
1328
|
+
}
|
|
1329
|
+
return dropped;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1392
1332
|
function devSystemPrompt() {
|
|
1393
1333
|
const live = loadWorkbench(devCtx.agentId);
|
|
1394
1334
|
const plugins = listPanelPlugins();
|
|
@@ -1470,12 +1410,24 @@ async function devChatLoop(session, emit, signal = null) {
|
|
|
1470
1410
|
if (interrupted()) { emit('interrupted', { at: `第 ${round + 1} 轮` }); return { reply: '(已被用户中断)', events }; }
|
|
1471
1411
|
emit('status', { text: `第 ${round + 1} 轮` });
|
|
1472
1412
|
const stream = models.stream(model, session.context, signal ? { signal } : undefined);
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1413
|
+
let final = null;
|
|
1414
|
+
try {
|
|
1415
|
+
for await (const ev of stream) {
|
|
1416
|
+
if (interrupted()) break;
|
|
1417
|
+
if (ev.type === 'text_delta') emit('text_delta', { delta: ev.delta });
|
|
1418
|
+
else if (ev.type === 'toolcall_end') emit('status', { text: `调用工具 ${ev.toolCall?.name || ''}` });
|
|
1419
|
+
else if (ev.type === 'error') {
|
|
1420
|
+
if (interrupted()) break; // 中断引发的流错误按中断处理,不报错
|
|
1421
|
+
throw new Error(`LLM 流错误: ${ev.error?.errorMessage || JSON.stringify(ev.error).slice(0, 200)}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
final = await stream.result();
|
|
1425
|
+
// pi-ai 把认证/请求失败表现为 stopReason=error 而非异常(同 llmProbe 判定)——不检查会被吞成静默空回复
|
|
1426
|
+
if (final?.stopReason === 'error') throw new Error(`LLM 请求失败: ${final.errorMessage || final.rawStopReason || 'stop=error'}`);
|
|
1427
|
+
} catch (e) {
|
|
1428
|
+
if (!interrupted()) throw e; // 真实错误上抛给路由 catch → SSE error → 前端 toast;中断路径用占位消息兜底
|
|
1477
1429
|
}
|
|
1478
|
-
|
|
1430
|
+
if (!final) final = { role: 'assistant', content: [], timestamp: Date.now() };
|
|
1479
1431
|
session.context.messages.push(final);
|
|
1480
1432
|
const calls = (final.content || []).filter(b => b.type === 'toolCall');
|
|
1481
1433
|
const text = (final.content || []).filter(b => b.type === 'text').map(b => b.text).join('');
|
|
@@ -1680,7 +1632,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1680
1632
|
if (pathname === '/api/health') return json(res, 200, { ok: true, auth_required: true }); // 前端据此弹登录门
|
|
1681
1633
|
return json(res, 401, { error: '需要访问令牌', code: 'AUTH_REQUIRED' });
|
|
1682
1634
|
}
|
|
1683
|
-
if (req.method === 'GET' && pathname === '/api/health') return json(res, 200, { ok: true, model: model?.id, arm:
|
|
1635
|
+
if (req.method === 'GET' && pathname === '/api/health') return json(res, 200, { ok: true, model: model?.id, arm: armApiBase(), pat: !!ARM_PAT, local_agents: loadRegistry().agents.length });
|
|
1684
1636
|
|
|
1685
1637
|
// ── ARM 连接与同步(07:大厅双视图 = 本地助手库 ∪ 远端目录)──
|
|
1686
1638
|
if (req.method === 'GET' && pathname === '/api/settings') {
|
|
@@ -1689,7 +1641,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1689
1641
|
return json(res, 200, {
|
|
1690
1642
|
arm: {
|
|
1691
1643
|
api_base: armApiBase(), api_base_runtime: !!cfg.arm_api_base, default_base: ARM_DEFAULT_BASE,
|
|
1692
|
-
mcp_url: armMcpUrl(), mcp_explicit: !!cfg.arm_mcp_url,
|
|
1693
1644
|
token_set: !!t, token_masked: t ? `${t.slice(0, 10)}…${t.slice(-4)}` : '',
|
|
1694
1645
|
token_source: cfg.arm_token ? 'runtime' : (env.ARM_API_TOKEN ? 'env' : (ARM_PAT ? 'pat' : 'none')),
|
|
1695
1646
|
},
|
|
@@ -1698,6 +1649,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1698
1649
|
model: llmModelName(), model_runtime: !!cfg.llm_model, catalog: llmCatalog,
|
|
1699
1650
|
key_set: !!k, key_masked: k ? `${k.slice(0, 6)}…${k.slice(-4)}` : '',
|
|
1700
1651
|
key_source: cfg.llm_api_key ? 'runtime' : (env.LLM_API_KEY ? 'env' : 'none'),
|
|
1652
|
+
context_tokens: llmContextTokens(), context_tokens_runtime: !!cfg.llm_context_tokens, default_context_tokens: LLM_DEFAULT_CONTEXT_TOKENS,
|
|
1701
1653
|
},
|
|
1702
1654
|
scene: { profile: sceneProfile(), length: sceneProfile().length },
|
|
1703
1655
|
auth: { enabled: !!accessToken(), token_masked: accessToken() ? `${accessToken().slice(0, 4)}…${accessToken().slice(-2)}` : '' },
|
|
@@ -1705,7 +1657,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1705
1657
|
});
|
|
1706
1658
|
}
|
|
1707
1659
|
if (req.method === 'PUT' && pathname === '/api/settings') {
|
|
1708
|
-
const { arm_api_base,
|
|
1660
|
+
const { arm_api_base, arm_token, llm_base_url, llm_api_key, llm_model, llm_context_tokens, scene_profile, access_token, guard } = await body(req);
|
|
1709
1661
|
// 守护栏维护:deny 正则可编译 / ro 须为单命令名;保存即热生效(classifyExec 每次调用现读配置);删减危险规则记入审计
|
|
1710
1662
|
let guardAudit = null;
|
|
1711
1663
|
if (guard !== undefined) {
|
|
@@ -1736,11 +1688,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1736
1688
|
if (u && !/^https?:\/\/[^\s/]+/.test(u)) return json(res, 400, { error: `站点地址须为 http(s) URL:${u}` });
|
|
1737
1689
|
cfg.arm_api_base = u;
|
|
1738
1690
|
}
|
|
1739
|
-
if (arm_mcp_url !== undefined) {
|
|
1740
|
-
const u = normUrl(arm_mcp_url);
|
|
1741
|
-
if (u && !/^https?:\/\/[^\s/]+/.test(u)) return json(res, 400, { error: `MCP 端点须为 http(s) URL:${u}` });
|
|
1742
|
-
cfg.arm_mcp_url = u;
|
|
1743
|
-
}
|
|
1744
1691
|
if (arm_token !== undefined) cfg.arm_token = String(arm_token || '').trim(); // 置空 = 回落 env/PAT
|
|
1745
1692
|
if (llm_base_url !== undefined) {
|
|
1746
1693
|
const u = normUrl(llm_base_url);
|
|
@@ -1752,6 +1699,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
1752
1699
|
if (m && !/^[\w.:@\/-]{1,80}$/.test(m)) return json(res, 400, { error: `模型名不合法:${m}` });
|
|
1753
1700
|
cfg.llm_model = m;
|
|
1754
1701
|
}
|
|
1702
|
+
if (llm_context_tokens !== undefined) { // 上下文窗口(token):驱动会话裁剪预算;留空/0 = 默认 128K。不影响 provider,无需热切换
|
|
1703
|
+
const raw = String(llm_context_tokens ?? '').trim();
|
|
1704
|
+
if (raw === '') cfg.llm_context_tokens = 0;
|
|
1705
|
+
else {
|
|
1706
|
+
const n = Math.floor(+raw);
|
|
1707
|
+
if (!Number.isFinite(n) || n < 4_000 || n > 2_000_000) return json(res, 400, { error: `上下文窗口须为 4000-2000000 的 token 数(留空 = 默认 ${LLM_DEFAULT_CONTEXT_TOKENS}):${raw}` });
|
|
1708
|
+
cfg.llm_context_tokens = n;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1755
1711
|
if (llm_api_key !== undefined) cfg.llm_api_key = String(llm_api_key || '').trim();
|
|
1756
1712
|
// LLM 热切换:先应用验证,失败整体回滚(应用只是构建对象,无网络请求)
|
|
1757
1713
|
let llm_probe = null;
|
|
@@ -1766,10 +1722,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1766
1722
|
try { llm_probe = await llmProbe(); } catch (e) { llm_probe = { online: false, error: String(e.message).slice(0, 150) }; }
|
|
1767
1723
|
}
|
|
1768
1724
|
saveCfg();
|
|
1769
|
-
audit({ actor: 'settings', action: 'save', arm_api_base: cfg.arm_api_base,
|
|
1725
|
+
audit({ actor: 'settings', action: 'save', arm_api_base: cfg.arm_api_base, llm_model: cfg.llm_model, llm_context_tokens: cfg.llm_context_tokens, llm_base_changed: llm_base_url !== undefined, token_changed: arm_token !== undefined || llm_api_key !== undefined, scene_profile_changed: sceneChanged, scene_profile_len: sceneProfile().length, auth_changed: authChanged, guard_changed: guardAudit });
|
|
1770
1726
|
// ARM 保存后即时探测(失败不回滚:允许先配置后启动 ARM)
|
|
1771
1727
|
let probe = { online: false, error: '' };
|
|
1772
|
-
if ([arm_api_base,
|
|
1728
|
+
if ([arm_api_base, arm_token].some((v) => v !== undefined)) {
|
|
1773
1729
|
try {
|
|
1774
1730
|
const t0 = Date.now();
|
|
1775
1731
|
await armRest('/api/v1/agents?page=1&pageSize=1', {}, 6000);
|
|
@@ -1777,7 +1733,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1777
1733
|
} catch (e) { probe = { online: false, error: String(e.message).slice(0, 120) }; }
|
|
1778
1734
|
}
|
|
1779
1735
|
const notes = [];
|
|
1780
|
-
if (probe.error) notes.push('ARM
|
|
1736
|
+
if (probe.error) notes.push('ARM 当前源不可达(目录/检索将回落本地库,源恢复后自动可用)');
|
|
1781
1737
|
if (llm_probe && !llm_probe.online) notes.push('LLM 已保存但探针未通过(密钥/端点/模型可用性待确认,对话可能失败,可再试或改回)');
|
|
1782
1738
|
return json(res, 200, { saved: true, probe, llm_probe, note: notes.join(';') });
|
|
1783
1739
|
}
|
|
@@ -1787,14 +1743,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1787
1743
|
try {
|
|
1788
1744
|
await armRest('/api/v1/agents?page=1&pageSize=1', {}, 6000);
|
|
1789
1745
|
online = true; latency_ms = Date.now() - t0;
|
|
1790
|
-
} catch {
|
|
1791
|
-
try { // 旧版 ARM 无 REST:回落 MCP 探测
|
|
1792
|
-
await mcpCall('search_agents', { keyword: '', limit: 1 }, { timeout_ms: 6000 });
|
|
1793
|
-
online = true; latency_ms = Date.now() - t0;
|
|
1794
|
-
} catch { }
|
|
1795
|
-
}
|
|
1746
|
+
} catch { }
|
|
1796
1747
|
const reg = loadRegistry();
|
|
1797
|
-
return json(res, 200, { online, latency_ms,
|
|
1748
|
+
return json(res, 200, { online, latency_ms, api: armApiBase(), pat: !!ARM_PAT, last_sync_at: reg.last_sync_at || null, local_count: reg.agents.length, outbox_pending: outboxCount() });
|
|
1798
1749
|
}
|
|
1799
1750
|
if (req.method === 'POST' && pathname === '/api/arm/sync') {
|
|
1800
1751
|
try { return json(res, 200, await armSync()); }
|
|
@@ -1806,7 +1757,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1806
1757
|
const kw = (url.searchParams.get('keyword') || '').trim().toLowerCase();
|
|
1807
1758
|
const reg = loadRegistry();
|
|
1808
1759
|
let online = true, remoteCards = [], authError = false;
|
|
1809
|
-
try { remoteCards = await armListAgents(url.searchParams.get('keyword') || ''); } // REST
|
|
1760
|
+
try { remoteCards = await armListAgents(url.searchParams.get('keyword') || ''); } // REST 全量
|
|
1810
1761
|
catch (e) { online = false; authError = /令牌未配置或无权限/.test(String(e.message)); } // 区分"未授权"与"不可达",前端引导配置令牌
|
|
1811
1762
|
const byId = new Map();
|
|
1812
1763
|
for (const e of reg.agents) {
|
|
@@ -1859,8 +1810,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1859
1810
|
};
|
|
1860
1811
|
const stream = models.stream(model, ctx);
|
|
1861
1812
|
let out = '';
|
|
1862
|
-
for await (const ev of stream) {
|
|
1813
|
+
for await (const ev of stream) {
|
|
1814
|
+
if (ev.type === 'text_delta') out += ev.delta;
|
|
1815
|
+
else if (ev.type === 'error') throw new Error(`LLM 流错误: ${ev.error?.errorMessage || JSON.stringify(ev.error).slice(0, 200)}`); // pi-ai 失败不抛异常,须显式拦截,否则 200 返回空 prompt
|
|
1816
|
+
}
|
|
1863
1817
|
const final = await stream.result();
|
|
1818
|
+
if (final?.stopReason === 'error') throw new Error(`LLM 请求失败: ${final.errorMessage || final.rawStopReason || 'stop=error'}`);
|
|
1864
1819
|
const text = ((final.content || []).filter(b => b.type === 'text').map(b => b.text).join('') || out).trim();
|
|
1865
1820
|
let draftName = '', prompt = text;
|
|
1866
1821
|
const jm = text.match(/\{[\s\S]*\}/); // JSON 容错解析:失败则整段当 prompt,不阻断
|
|
@@ -2082,7 +2037,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
2082
2037
|
const toolsAvail = dep.toolset.map(t => t.tool).join(', ');
|
|
2083
2038
|
const sys = '你是工业智能体平台的工作台设计师。根据助手档案生成工作台 DSL(JSON)。面板类型只允许: realtime_list(实时告警流)/status_grid(设备状态矩阵)/agent_dialog(诊断对话)/action_group(操作区)/kpi_strip(指标条)/inspection_report(自检报告)。结构: {"workbench":"名称","owner_agent":"agentId","version":"v0.1-draft","layout":[...],"panels":[{"id","type","title"}...]}。action_group 的 panels 含 "actions":[{"id","label","orchestration","autonomy","confirm":{"text","double_check"}}],orchestration 只能引用这些: logic://设备复位流程@mock / logic://告警派工@mock / logic://服务滚动重启@mock / logic://环境自检@mock。只输出 JSON,不要 markdown 代码围栏。';
|
|
2084
2039
|
const user = `助手: ${arm.name} ${arm.version}\n职责: ${arm.prompt}\n可用工具: ${toolsAvail}\n默认自主级别: ${dep.default_autonomy}\n请生成 4-6 个面板,含一个 agent_dialog 和一个 action_group。`;
|
|
2085
|
-
|
|
2040
|
+
let out;
|
|
2041
|
+
try { out = await llmComplete(sys, user); }
|
|
2042
|
+
catch (e) { return json(res, 502, { error: `AI 草稿生成失败: ${e.message}` }); }
|
|
2086
2043
|
const cleaned = out.replace(/```json|```/g, '').trim();
|
|
2087
2044
|
let draft;
|
|
2088
2045
|
try { draft = JSON.parse(cleaned); } catch (e) { return json(res, 502, { error: 'AI 草稿解析失败', raw: cleaned.slice(0, 500) }); }
|
|
@@ -2409,6 +2366,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
2409
2366
|
s.context.systemPrompt = devSystemPrompt();
|
|
2410
2367
|
}
|
|
2411
2368
|
s.context.messages.push({ role: 'user', content: message, timestamp: Date.now() });
|
|
2369
|
+
const dropped = trimContext(s.context);
|
|
2370
|
+
if (dropped > 0) emit('status', { text: `会话历史过长,已自动裁剪最早 ${dropped} 条消息` });
|
|
2412
2371
|
await devChatLoop(s, emit, ctl.signal);
|
|
2413
2372
|
emit('meta', { hasDraft: !!loadDraft(agentId) });
|
|
2414
2373
|
} catch (e) { emit('error', { message: e.message }); }
|
|
@@ -2438,6 +2397,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
2438
2397
|
return res.end();
|
|
2439
2398
|
}
|
|
2440
2399
|
s.context.messages.push({ role: 'user', content: context ? `${message}\n(界面注入上下文:当前选中设备 ${context.device})` : message, timestamp: Date.now() });
|
|
2400
|
+
const dropped = trimContext(s.context); // 主对话 sessionId 同样固定(chat-<agentId>-<wsId>),不裁剪会同样撑死
|
|
2401
|
+
if (dropped > 0) emit('status', { text: `会话历史过长,已自动裁剪最早 ${dropped} 条消息` });
|
|
2441
2402
|
s.running = true; s.sinceTs = Date.now(); s.abortCtl = ctl;
|
|
2442
2403
|
const out = await agentLoop(s, deployment, emit, ctl.signal);
|
|
2443
2404
|
emit('done', { reply: out.reply, pending: out.pending });
|
|
@@ -2481,6 +2442,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
2481
2442
|
finally { finished = true; if (s) { s.running = false; s.statusText = ''; s.abortCtl = null; } }
|
|
2482
2443
|
return res.end();
|
|
2483
2444
|
}
|
|
2445
|
+
// ── 会话清空:主动丢弃上下文重开(上下文污染/换思路);运行中拒绝(先中断),pending 确认卡随会话一并作废 ──
|
|
2446
|
+
if (req.method === 'POST' && pathname === '/api/session/reset') {
|
|
2447
|
+
const { scope, sessionId } = await body(req);
|
|
2448
|
+
if (!sessionId || !String(sessionId).trim()) return json(res, 400, { error: '缺少 sessionId' });
|
|
2449
|
+
const store = scope === 'dev' ? devSessions : sessions;
|
|
2450
|
+
const s = store.get(String(sessionId).trim());
|
|
2451
|
+
if (!s) return json(res, 200, { ok: true, note: '会话本就为空' });
|
|
2452
|
+
if (s.running) return json(res, 409, { error: '会话正在处理中,请先中断再清空' });
|
|
2453
|
+
store.delete(String(sessionId).trim());
|
|
2454
|
+
audit({ actor: 'user', action: 'session-reset', scope: scope === 'dev' ? 'dev' : 'chat', sessionId: String(sessionId).slice(0, 48) });
|
|
2455
|
+
return json(res, 200, { ok: true });
|
|
2456
|
+
}
|
|
2484
2457
|
// ── 会话历史与进度(切换助手后回来:上下文回放 + 后台运行状态;服务端为唯一事实源)──
|
|
2485
2458
|
if (req.method === 'GET' && pathname === '/api/chat/history') {
|
|
2486
2459
|
const sid = url.searchParams.get('sessionId');
|
|
@@ -2533,18 +2506,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
2533
2506
|
if (!s) return json(res, 400, { error: '会话不存在' });
|
|
2534
2507
|
const convo = s.context.messages.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
2535
2508
|
.slice(-12).map(m => `${m.role === 'user' ? '用户' : '助手'}: ${(m.content || []).filter?.(b => b.type === 'text').map(b => b.text).join('') ?? m.content}`).join('\n');
|
|
2536
|
-
|
|
2509
|
+
let md;
|
|
2510
|
+
try { md = await llmComplete('你是经验沉淀器。把运维对话提炼为一条结构化经验 markdown,章节固定:## 现象 / ## 根因 / ## 处置步骤 / ## 验证 / ## 预防。事实不确定就写"待确认"。只输出 markdown 正文。', convo || '(空会话)'); }
|
|
2511
|
+
catch (e) { return json(res, 502, { error: `经验起草失败: ${e.message}` }); }
|
|
2537
2512
|
return json(res, 200, { markdown: md });
|
|
2538
2513
|
}
|
|
2539
2514
|
if (req.method === 'POST' && pathname === '/api/experience/commit') {
|
|
2540
2515
|
const { agentId, name, description, content } = await body(req);
|
|
2541
2516
|
const arm = agentProfileNow(agentId);
|
|
2542
2517
|
const full = ['---', 'kind: experience', 'source: aims-platform-mvp', `agent: ${arm.name || agentId}@${arm.version || ''}`, 'scenario: 立库A区智能运维@mvp', '---', '', content].join('\n');
|
|
2543
|
-
const r = await
|
|
2544
|
-
|
|
2545
|
-
metric({ kind: 'experience', agentId, title: name, knowledge_id: kid });
|
|
2518
|
+
const r = await armCreateKnowledge({ name, description: description || `工作台人工审阅沉淀`, content: full, tags: ['MVP回流'] });
|
|
2519
|
+
metric({ kind: 'experience', agentId, title: name, knowledge_id: r.id });
|
|
2546
2520
|
audit({ actor: 'experience-reflow', target: 'ARM', name });
|
|
2547
|
-
return json(res, 200, { result: r });
|
|
2521
|
+
return json(res, 200, { result: `已创建 ${r.name}(id: ${r.id})${r.shareUrl ? `\n${r.shareUrl}` : ''}`, knowledge_id: r.id, share_url: r.shareUrl || '' });
|
|
2548
2522
|
}
|
|
2549
2523
|
// ── 自进化看板 ──
|
|
2550
2524
|
if (req.method === 'GET' && pathname === '/api/evolution') {
|
|
@@ -2623,7 +2597,7 @@ applyLLMConfig().then(() => {
|
|
|
2623
2597
|
server.listen(PORT, HOST, () => {
|
|
2624
2598
|
console.log(`[aims-workstation] http://localhost:${PORT}(绑定 ${HOST}) | 模型: ${model.id} @ ${model.baseUrl}`);
|
|
2625
2599
|
console.log(`[aims-workstation] 数据目录 AIMS_HOME: ${AIMS_HOME}`);
|
|
2626
|
-
|
|
2600
|
+
console.log(`[aims-workstation] ARM(REST): ${armApiBase()} | PAT: ${ARM_PAT ? '已加载' : '未配置'} | 已上岗助手: ${fs.readdirSync(DEPLOY_DIR).length} | 本地助手库: ${loadRegistry().agents.length}`);
|
|
2627
2601
|
});
|
|
2628
2602
|
// 已上岗助手后台物化为本地档案(best-effort;ARM 不可达时静默跳过,待手动同步)
|
|
2629
2603
|
(async () => {
|