aims-workstation 0.1.0 → 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` 依赖)。服务默认只绑定 `127.0.0.1`(本机工具,勿暴露局域网)。
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.0",
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 || '127.0.0.1'; // 本机工具默认只绑回环(exec/目录浏览/面板动作不宜暴露局域网)
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 < 24; round++) {
1172
- emit('status', { text: `第 ${round + 1}/24 轮` });
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 = 16) {
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: '(达到工具调用轮次上限)', pending: null };
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) || 16));
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) || 16));
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 = ''; } }