aims-workstation 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npx aims-workstation
|
|
|
19
19
|
# 打开 http://127.0.0.1:5600 → 系统设置里配置 ARM 源与 LLM 即可
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
要求 **Node ≥ 22.5**(`node:sqlite`
|
|
22
|
+
要求 **Node ≥ 22.5**(`node:sqlite` 依赖)。服务默认监听 `0.0.0.0`(局域网可访问,适合团队共用);仅本机使用请设 `AIMS_HOST=127.0.0.1`。
|
|
23
23
|
|
|
24
24
|
## 配置
|
|
25
25
|
|
|
@@ -35,6 +35,7 @@ npx aims-workstation
|
|
|
35
35
|
|
|
36
36
|
## 安全模型
|
|
37
37
|
|
|
38
|
+
- **访问令牌**(可选):设置页「访问安全」启用后,所有 API 需认证——浏览器登录换 HttpOnly Cookie,脚本用 `Authorization: Bearer <token>`;替换令牌即踢掉所有已登录设备
|
|
38
39
|
- 助手凭据与提示词分离;密钥只存本机(界面永远掩码)
|
|
39
40
|
- 写操作 L2 挂起确认;毁灭性命令黑名单全级别拒绝
|
|
40
41
|
- 面板代理出网白名单(仅插件声明过的主机);数据集按助手×工作区物理隔离
|
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.2",
|
|
17
17
|
"description": "AIMS 工作站:本地智能体工作站——ARM 助手本地化(下载/离线/升级)、工作台对话式定制、多工作区隔离、分级自主与审计",
|
|
18
18
|
"bin": {
|
|
19
19
|
"aims-workstation": "bin/aims-workstation.js"
|
package/server.js
CHANGED
|
@@ -69,11 +69,11 @@ for (const envFile of [path.join(PKG_ROOT, '.env'), path.join(AIMS_HOME, '.env')
|
|
|
69
69
|
} catch { /* .env 可选 */ }
|
|
70
70
|
}
|
|
71
71
|
const PORT = +(process.env.PORT || env.PORT || 5600);
|
|
72
|
-
const HOST = process.env.AIMS_HOST || '
|
|
72
|
+
const HOST = process.env.AIMS_HOST || '0.0.0.0'; // 默认局域网可达(团队共用);仅本机使用可设 AIMS_HOST=127.0.0.1
|
|
73
73
|
const ARM_MCP_URL = process.env.ARM_MCP_URL || env.ARM_MCP_URL;
|
|
74
74
|
// ── 运行时配置(ARM 源与 LLM 均可私有化部署:在「系统设置」页切换,存 AIMS_HOME/config/platform.json,优先级 运行时 > 环境变量 > .env)──
|
|
75
75
|
const CFG_FILE = path.join(AIMS_HOME, 'config/platform.json');
|
|
76
|
-
const cfg = { arm_api_base: '', arm_mcp_url: '', arm_token: '', llm_base_url: '', llm_api_key: '', llm_model: '', scene_profile: '' };
|
|
76
|
+
const cfg = { arm_api_base: '', arm_mcp_url: '', arm_token: '', llm_base_url: '', llm_api_key: '', llm_model: '', scene_profile: '', access_token: '' };
|
|
77
77
|
function loadCfg() { try { Object.assign(cfg, JSON.parse(fs.readFileSync(CFG_FILE, 'utf8'))); } catch { } }
|
|
78
78
|
loadCfg();
|
|
79
79
|
function saveCfg() { fs.writeFileSync(CFG_FILE, JSON.stringify(cfg, null, 2)); }
|
|
@@ -90,6 +90,17 @@ const llmModelName = () => cfg.llm_model || env.LLM_MODEL || 'glm-5.3';
|
|
|
90
90
|
// 与知识检索互补(常驻事实 vs 按需检索);凭据不进画像(放"凭据在哪、找谁要")。
|
|
91
91
|
const sceneProfile = () => String(cfg.scene_profile || '').trim();
|
|
92
92
|
const SCENE_PROFILE_MAX = 4000;
|
|
93
|
+
// 访问令牌:启用后所有 /api/* 需认证(Cookie 或 Bearer),静态页面不拦;局域网开放时的基本防护
|
|
94
|
+
const accessToken = () => String(cfg.access_token || '').trim();
|
|
95
|
+
const AUTH_COOKIE = 'aims_auth';
|
|
96
|
+
const authCookieValue = () => crypto.createHash('sha256').update('aims:' + accessToken()).digest('hex');
|
|
97
|
+
const authOk = (req) => {
|
|
98
|
+
if (!accessToken()) return true; // 未启用=开放
|
|
99
|
+
const cookie = String(req.headers.cookie || '');
|
|
100
|
+
if (cookie.includes(`${AUTH_COOKIE}=${authCookieValue()}`)) return true;
|
|
101
|
+
return req.headers.authorization === `Bearer ${accessToken()}`;
|
|
102
|
+
};
|
|
103
|
+
const safeEqual = (a, b) => String(a).length === String(b).length && crypto.timingSafeEqual(Buffer.from(String(a)), Buffer.from(String(b)));
|
|
93
104
|
let ARM_PAT = env.ARM_PAT || '';
|
|
94
105
|
if (!ARM_PAT) {
|
|
95
106
|
try { ARM_PAT = JSON.parse(fs.readFileSync(path.join(process.env.HOME, '.arm/config.json'), 'utf8')).token || ''; }
|
|
@@ -1168,8 +1179,8 @@ function devSystemPrompt() {
|
|
|
1168
1179
|
async function devChatLoop(session, emit) {
|
|
1169
1180
|
const events = [];
|
|
1170
1181
|
const tools = Object.entries(DEV_TOOLS).map(([n, t]) => ({ name: n, description: t.desc, parameters: { type: 'object', properties: t.schema, required: t.required || [] } }));
|
|
1171
|
-
for (let round = 0; round <
|
|
1172
|
-
emit('status', { text: `第 ${round + 1}/
|
|
1182
|
+
for (let round = 0; round < 32; round++) {
|
|
1183
|
+
emit('status', { text: `第 ${round + 1}/32 轮` });
|
|
1173
1184
|
const stream = models.stream(model, session.context);
|
|
1174
1185
|
for await (const ev of stream) {
|
|
1175
1186
|
if (ev.type === 'text_delta') emit('text_delta', { delta: ev.delta });
|
|
@@ -1263,7 +1274,7 @@ async function ensureSession(sessionId, agentId, autonomyLevel, wsId) {
|
|
|
1263
1274
|
}
|
|
1264
1275
|
|
|
1265
1276
|
// 运行一轮 Agent 循环,事件通过 emit(type, data) 推给 SSE;s.running/s.statusText 供跨页面进度跟踪
|
|
1266
|
-
async function agentLoop(s, deployment, emit, maxRounds =
|
|
1277
|
+
async function agentLoop(s, deployment, emit, maxRounds = 32) {
|
|
1267
1278
|
for (let round = 0; round < maxRounds; round++) {
|
|
1268
1279
|
s.statusText = `第 ${round + 1}/${maxRounds} 轮 · 推理中`;
|
|
1269
1280
|
const stream = models.stream(model, s.context);
|
|
@@ -1307,7 +1318,7 @@ async function agentLoop(s, deployment, emit, maxRounds = 16) {
|
|
|
1307
1318
|
audit({ actor: 'assistant', agent: deployment.agent_id, tool: tc.name, autonomy: s.autonomy, ok: !isErr });
|
|
1308
1319
|
}
|
|
1309
1320
|
}
|
|
1310
|
-
return { reply: '
|
|
1321
|
+
return { reply: '(达到工具调用轮次上限——回复「继续」可在完整上下文上续跑;也可在工作台 DSL 提高 max_rounds,上限 128)', pending: null };
|
|
1311
1322
|
}
|
|
1312
1323
|
|
|
1313
1324
|
// ── HTTP ────────────────────────────────────────────────
|
|
@@ -1333,6 +1344,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
1333
1344
|
}
|
|
1334
1345
|
return json(res, 404, { error: '前端未构建:cd web && pnpm build' });
|
|
1335
1346
|
}
|
|
1347
|
+
// ── 访问认证(启用令牌后:API 需 Cookie/Bearer;静态页面开放;health 化身只报 auth_required)──
|
|
1348
|
+
if (req.method === 'POST' && pathname === '/api/auth/login') {
|
|
1349
|
+
const { token } = await body(req);
|
|
1350
|
+
if (!accessToken()) return json(res, 200, { ok: true, auth_required: false });
|
|
1351
|
+
if (!token || !safeEqual(token, accessToken())) return json(res, 401, { error: '访问令牌不正确' });
|
|
1352
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Set-Cookie': `${AUTH_COOKIE}=${authCookieValue()}; HttpOnly; Path=/; SameSite=Lax; Max-Age=2592000` });
|
|
1353
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
1354
|
+
}
|
|
1355
|
+
if (!authOk(req) && pathname.startsWith('/api')) {
|
|
1356
|
+
if (pathname === '/api/health') return json(res, 200, { ok: true, auth_required: true }); // 前端据此弹登录门
|
|
1357
|
+
return json(res, 401, { error: '需要访问令牌', code: 'AUTH_REQUIRED' });
|
|
1358
|
+
}
|
|
1336
1359
|
if (req.method === 'GET' && pathname === '/api/health') return json(res, 200, { ok: true, model: model?.id, arm: armMcpUrl(), pat: !!ARM_PAT, local_agents: loadRegistry().agents.length });
|
|
1337
1360
|
|
|
1338
1361
|
// ── ARM 连接与同步(07:大厅双视图 = 本地助手库 ∪ 远端目录)──
|
|
@@ -1353,10 +1376,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1353
1376
|
key_source: cfg.llm_api_key ? 'runtime' : (env.LLM_API_KEY ? 'env' : 'none'),
|
|
1354
1377
|
},
|
|
1355
1378
|
scene: { profile: sceneProfile(), length: sceneProfile().length },
|
|
1379
|
+
auth: { enabled: !!accessToken(), token_masked: accessToken() ? `${accessToken().slice(0, 4)}…${accessToken().slice(-2)}` : '' },
|
|
1356
1380
|
});
|
|
1357
1381
|
}
|
|
1358
1382
|
if (req.method === 'PUT' && pathname === '/api/settings') {
|
|
1359
|
-
const { arm_api_base, arm_mcp_url, arm_token, llm_base_url, llm_api_key, llm_model, scene_profile } = await body(req);
|
|
1383
|
+
const { arm_api_base, arm_mcp_url, arm_token, llm_base_url, llm_api_key, llm_model, scene_profile, access_token } = await body(req);
|
|
1384
|
+
let authChanged = false;
|
|
1385
|
+
if (access_token !== undefined) { const t = String(access_token || '').trim(); authChanged = t !== accessToken(); cfg.access_token = t; } // 置空=关闭认证
|
|
1360
1386
|
const normUrl = (u) => { const s = String(u || '').trim().replace(/\/+$/, ''); return s; };
|
|
1361
1387
|
let sceneChanged = false;
|
|
1362
1388
|
if (scene_profile !== undefined) {
|
|
@@ -1399,7 +1425,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1399
1425
|
try { llm_probe = await llmProbe(); } catch (e) { llm_probe = { online: false, error: String(e.message).slice(0, 150) }; }
|
|
1400
1426
|
}
|
|
1401
1427
|
saveCfg();
|
|
1402
|
-
audit({ actor: 'settings', action: 'save', arm_api_base: cfg.arm_api_base, mcp_explicit: !!cfg.arm_mcp_url, llm_model: cfg.llm_model, 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 });
|
|
1428
|
+
audit({ actor: 'settings', action: 'save', arm_api_base: cfg.arm_api_base, mcp_explicit: !!cfg.arm_mcp_url, llm_model: cfg.llm_model, 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 });
|
|
1403
1429
|
// ARM 保存后即时探测(失败不回滚:允许先配置后启动 ARM)
|
|
1404
1430
|
let probe = { online: false, error: '' };
|
|
1405
1431
|
if ([arm_api_base, arm_mcp_url, arm_token].some((v) => v !== undefined)) {
|
|
@@ -1885,7 +1911,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1885
1911
|
s.context.messages.push({ role: 'user', content: context ? `${message}\n(界面注入上下文:当前选中设备 ${context.device})` : message, timestamp: Date.now() });
|
|
1886
1912
|
const wb = loadDraft(agentId) || loadWorkbench(agentId) || {};
|
|
1887
1913
|
s.running = true; s.sinceTs = Date.now();
|
|
1888
|
-
const out = await agentLoop(s, deployment, emit, Math.max(1, +(wb.max_rounds) ||
|
|
1914
|
+
const out = await agentLoop(s, deployment, emit, Math.min(128, Math.max(1, +(wb.max_rounds) || 32)));
|
|
1889
1915
|
emit('done', { reply: out.reply, pending: out.pending });
|
|
1890
1916
|
} catch (e) { emit('error', { message: e.message }); }
|
|
1891
1917
|
finally { const s = sessions.get(sessionId); if (s) { s.running = false; s.statusText = ''; } }
|
|
@@ -1912,7 +1938,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1912
1938
|
}
|
|
1913
1939
|
const wb = loadDraft(s.agentId) || loadWorkbench(s.agentId) || {};
|
|
1914
1940
|
s.running = true; s.sinceTs = Date.now();
|
|
1915
|
-
const out = await agentLoop(s, deployment, emit, Math.max(1, +(wb.max_rounds) ||
|
|
1941
|
+
const out = await agentLoop(s, deployment, emit, Math.min(128, Math.max(1, +(wb.max_rounds) || 32)));
|
|
1916
1942
|
emit('done', { reply: out.reply, pending: out.pending });
|
|
1917
1943
|
} catch (e) { emit('error', { message: e.message }); }
|
|
1918
1944
|
finally { if (s) { s.running = false; s.statusText = ''; } }
|