aims-workstation 0.1.4 → 0.1.6

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
@@ -2,15 +2,19 @@
2
2
  PORT=5600
3
3
 
4
4
  # 平台 LLM(OpenAI 兼容端点;也可在「系统设置」页运行时切换)
5
- LLM_BASE_URL=https://open.bigmodel.cn/api/coding/paas/v4
6
- LLM_API_KEY=<your-llm-api-key>
7
- LLM_MODEL=glm-5.3
5
+ # 缺省公司内网 bifrost 网关(内网可达即开箱可用);以下三行留空/删除即用默认
6
+ LLM_BASE_URL=http://bifrost.infra.dev.aimstek.cn/v1
7
+ LLM_API_KEY=def
8
+ LLM_MODEL=def
8
9
 
9
- # ARM 系统(人事系统:助手档案 + 经验库)
10
- ARM_MCP_URL=https://your-arm-host/api/mcp
11
- # PAT 留空则自动读取 ~/.arm/config.json 的 token(仅经验写入需要)
10
+ # ARM 系统(助手档案 + 经验库):站点地址缺省公司内网 http://arm.agent-platform.dev.aimstek.cn,留空即用默认
11
+ # 匿名可浏览目录;下载/上岗/新建/经验回流需令牌——到 ARM 生成访问令牌(arm_pat_…)后填入,或在「系统设置 · ARM 源」粘贴
12
+ # ARM_MCP_URL=https://your-arm-host/api/mcp # 可选:私有化部署时覆盖
13
+ # ARM_API_BASE=http://localhost:3001 # 可选:本地联调 ARM 时启用
14
+ # PAT 留空则自动读取 ~/.arm/config.json 的 token
12
15
  ARM_PAT=
13
-
14
- # ARM REST API 默认值(v2:助手检索/下载 + 知识混合检索;运行时可在「系统设置」页切换源)
15
- ARM_API_BASE=http://localhost:3001
16
16
  ARM_API_TOKEN=<arm_pat_xxx-or-leave-empty>
17
+
18
+ # npm 发版令牌(npmjs.com → Access Tokens 生成;供 scripts/publish-npm.sh 使用,发布免浏览器授权)
19
+ # 选 Granular(限定本包、Read and write)或 Classic → Automation
20
+ # NPM_TOKEN=npm_xxxxxxxxxxxxxxxxxxxx
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.4",
16
+ "version": "0.1.6",
17
17
  "description": "AIMS 工作站:本地智能体工作站——ARM 助手本地化(下载/离线/升级)、工作台对话式定制、多工作区隔离、分级自主与审计",
18
18
  "bin": {
19
19
  "aims-workstation": "bin/aims-workstation.js"
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # 发布到 npm 公共仓(令牌免浏览器 2FA 授权)
3
+ #
4
+ # 令牌配置(一次性):在包根目录 .env 里加一行
5
+ # NPM_TOKEN=npm_xxxxxxxxxxxx
6
+ # 令牌到 npmjs.com → 头像 → Access Tokens 生成:
7
+ # 推荐 Granular(限定 aims-workstation 包、Read and write、可设过期时间);
8
+ # 选 Classic 时必须用 Automation 类型——Publish 类型在 2FA 开启下仍会要求验证码。
9
+ # CI/临时覆盖可用环境变量:NPM_TOKEN=npm_xxx ./scripts/publish-npm.sh
10
+ # ⚠ 令牌不要写进本文件:scripts/ 在 package.json 的 files 清单里,会随 npm 包发布出去
11
+ #
12
+ # 用法(在 aims-workstation/ 目录下):
13
+ # ./scripts/publish-npm.sh 发布当前版本
14
+ # ./scripts/publish-npm.sh patch 先升版本(patch/minor/major/1.2.3)再发布
15
+ set -euo pipefail
16
+ cd "$(dirname "$0")/.."
17
+
18
+ REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org/}"
19
+
20
+ # 令牌:环境变量 > .env 的 NPM_TOKEN(不 source 整个 .env,只取这一行)
21
+ TOKEN="${NPM_TOKEN:-}"
22
+ if [[ -z "$TOKEN" && -f .env ]]; then
23
+ TOKEN="$(sed -n 's/^NPM_TOKEN=//p' .env | tail -n1 | tr -d "[:space:]\"'")"
24
+ fi
25
+ if [[ -z "$TOKEN" ]]; then
26
+ echo "✗ 未找到 NPM_TOKEN(环境变量或 .env)。到 npmjs.com → Access Tokens 生成后执行:"
27
+ echo " echo 'NPM_TOKEN=npm_你的令牌' >> .env"
28
+ exit 1
29
+ fi
30
+ case "$TOKEN" in
31
+ npm_*) ;;
32
+ *) echo "⚠ 令牌不以 npm_ 开头(npmjs.org 令牌通常以 npm_ 开头),请确认来源" ;;
33
+ esac
34
+
35
+ if [[ $# -ge 1 ]]; then
36
+ npm version "$1" --no-git-tag-version
37
+ fi
38
+ NAME="$(node -p "require('./package.json').name")"
39
+ VERSION="$(node -p "require('./package.json').version")"
40
+ echo "→ 发布 $NAME@$VERSION → $REGISTRY"
41
+
42
+ # 令牌经临时 userconfig 传入,不写 ~/.npmrc、不落盘
43
+ CFG="$(mktemp)"
44
+ trap 'rm -f "$CFG"' EXIT
45
+ HOST_KEY="$(echo "$REGISTRY" | sed -E 's|^https?://||; s|/+$||')"
46
+ printf '//%s/:_authToken=%s\n' "$HOST_KEY" "$TOKEN" > "$CFG"
47
+
48
+ echo "→ 身份: $(NPM_CONFIG_USERCONFIG="$CFG" npm whoami --registry="$REGISTRY" 2>/dev/null || echo '未知(Granular 令牌不支持 whoami,属正常,直接尝试发布)')"
49
+ NPM_CONFIG_USERCONFIG="$CFG" npm publish --registry="$REGISTRY"
50
+ echo "✅ 已发布 $NAME@$VERSION"
51
+ echo " 安装: npx $NAME@$VERSION"
52
+ if [[ $# -ge 1 ]]; then
53
+ echo "ℹ 版本号已变更,记得提交 package.json / package-lock.json"
54
+ fi
package/server.js CHANGED
@@ -67,13 +67,13 @@ for (const envFile of [path.join(PKG_ROOT, '.env'), path.join(AIMS_HOME, '.env')
67
67
  try {
68
68
  for (const line of fs.readFileSync(envFile, 'utf8').split('\n')) {
69
69
  const m = line.match(/^\s*([A-Z_]+)\s*=\s*(.*)\s*$/);
70
- if (m && env[m[1]] === undefined) env[m[1]] = m[2];
70
+ // 跳过模板占位符(<xxx-or-leave-empty> 之类):直接复制 .env.example 为 .env 时不应产生毒害值
71
+ if (m && env[m[1]] === undefined && !/^<[^>]*>$/.test(m[2])) env[m[1]] = m[2];
71
72
  }
72
73
  } catch { /* .env 可选 */ }
73
74
  }
74
75
  const PORT = +(process.env.PORT || env.PORT || 5600);
75
76
  const HOST = process.env.AIMS_HOST || '0.0.0.0'; // 默认局域网可达(团队共用);仅本机使用可设 AIMS_HOST=127.0.0.1
76
- const ARM_MCP_URL = process.env.ARM_MCP_URL || env.ARM_MCP_URL;
77
77
  // ── 运行时配置(ARM 源与 LLM 均可私有化部署:在「系统设置」页切换,存 AIMS_HOME/config/platform.json,优先级 运行时 > 环境变量 > .env)──
78
78
  const CFG_FILE = path.join(AIMS_HOME, 'config/platform.json');
79
79
  const cfg = { arm_api_base: '', arm_mcp_url: '', arm_token: '', llm_base_url: '', llm_api_key: '', llm_model: '', scene_profile: '', access_token: '' };
@@ -96,15 +96,16 @@ function loadCfg() { try { Object.assign(cfg, JSON.parse(fs.readFileSync(CFG_FIL
96
96
  loadCfg();
97
97
  const guardNow = () => cfg.guard;
98
98
  function saveCfg() { fs.writeFileSync(CFG_FILE, JSON.stringify(cfg, null, 2)); }
99
- // 站点地址:运行时配置 > ARM_API_BASE(env) > 从 MCP URL 推导;MCP 端点:显式配置 > 跟随站点地址 > env
100
- const armApiBase = () => cfg.arm_api_base || process.env.ARM_API_BASE || env.ARM_API_BASE || String(env.ARM_MCP_URL || '').replace(/\/api\/mcp$/, '');
101
- const armMcpUrl = () => cfg.arm_mcp_url || (cfg.arm_api_base ? `${cfg.arm_api_base}/api/mcp` : ARM_MCP_URL);
99
+ // ARM 站点地址:运行时配置 > ARM_API_BASE(env) > 从 MCP URL 推导 > 公司内网默认(内网可达即开箱可用);MCP 端点:显式配置 > 跟随站点地址
100
+ 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 || (env.ARM_MCP_URL ? String(env.ARM_MCP_URL).replace(/\/api\/mcp$/, '') : ARM_DEFAULT_BASE);
102
+ const armMcpUrl = () => cfg.arm_mcp_url || `${armApiBase()}/api/mcp`;
102
103
  const armApiToken = () => cfg.arm_token || env.ARM_API_TOKEN || ARM_PAT;
103
- // LLM:OpenAI 兼容端点(baseUrl/模型/密钥),缺省智谱 coding plan
104
- const LLM_DEFAULT_BASE = 'https://open.bigmodel.cn/api/coding/paas/v4';
104
+ // LLM:OpenAI 兼容端点(baseUrl/模型/密钥),缺省公司内网 bifrost 网关(内网可达即可用,无需配置)
105
+ const LLM_DEFAULT_BASE = 'http://bifrost.infra.dev.aimstek.cn/v1';
105
106
  const llmBaseUrl = () => cfg.llm_base_url || env.LLM_BASE_URL || LLM_DEFAULT_BASE;
106
- const llmApiKey = () => cfg.llm_api_key || env.LLM_API_KEY || '';
107
- const llmModelName = () => cfg.llm_model || env.LLM_MODEL || 'glm-5.3';
107
+ const llmApiKey = () => cfg.llm_api_key || env.LLM_API_KEY || 'def';
108
+ const llmModelName = () => cfg.llm_model || env.LLM_MODEL || 'def';
108
109
  // 场景画像:平台级、全局一份的用户场景描述(团队环境/系统清单/规范),注入所有助手;空则不注入。
109
110
  // 与知识检索互补(常驻事实 vs 按需检索);凭据不进画像(放"凭据在哪、找谁要")。
110
111
  const sceneProfile = () => String(cfg.scene_profile || '').trim();
@@ -214,7 +215,10 @@ async function armRest(pathname, init = {}, timeout_ms = 20000) {
214
215
  try {
215
216
  const res = await fetch(armApiBase() + pathname, { ...init, headers, signal: ctrl.signal });
216
217
  const data = await res.json();
217
- if (!res.ok || data.ok === false) throw new Error(`ARM REST ${pathname.split('?')[0]}: ${data.msg || res.status}`);
218
+ if (!res.ok || data.ok === false) {
219
+ const noAuth = res.status === 401 || res.status === 403 || /401|403|unauthorized|forbidden/i.test(String(data.msg || ''));
220
+ throw new Error(`ARM REST ${pathname.split('?')[0]}: ${data.msg || res.status}${noAuth ? '——令牌未配置或无权限:到 ARM(' + armApiBase() + ')生成访问令牌(arm_pat_…)后,粘贴到「系统设置 · ARM 源 · 访问令牌」' : ''}`);
221
+ }
218
222
  return data.data;
219
223
  } catch (e) {
220
224
  if (e.message === 'fetch failed') { // undici 把网络层错误笼统包装,展开真实原因(DNS/连接/TLS)
@@ -1449,7 +1453,7 @@ const server = http.createServer(async (req, res) => {
1449
1453
  const k = llmApiKey();
1450
1454
  return json(res, 200, {
1451
1455
  arm: {
1452
- api_base: armApiBase(), api_base_runtime: !!cfg.arm_api_base,
1456
+ api_base: armApiBase(), api_base_runtime: !!cfg.arm_api_base, default_base: ARM_DEFAULT_BASE,
1453
1457
  mcp_url: armMcpUrl(), mcp_explicit: !!cfg.arm_mcp_url,
1454
1458
  token_set: !!t, token_masked: t ? `${t.slice(0, 10)}…${t.slice(-4)}` : '',
1455
1459
  token_source: cfg.arm_token ? 'runtime' : (env.ARM_API_TOKEN ? 'env' : (ARM_PAT ? 'pat' : 'none')),
@@ -1566,9 +1570,9 @@ const server = http.createServer(async (req, res) => {
1566
1570
  if (req.method === 'GET' && pathname === '/api/agents') {
1567
1571
  const kw = (url.searchParams.get('keyword') || '').trim().toLowerCase();
1568
1572
  const reg = loadRegistry();
1569
- let online = true, remoteCards = [];
1573
+ let online = true, remoteCards = [], authError = false;
1570
1574
  try { remoteCards = await armListAgents(url.searchParams.get('keyword') || ''); } // REST 全量优先,旧版回落 MCP
1571
- catch { online = false; }
1575
+ catch (e) { online = false; authError = /令牌未配置或无权限/.test(String(e.message)); } // 区分"未授权"与"不可达",前端引导配置令牌
1572
1576
  const byId = new Map();
1573
1577
  for (const e of reg.agents) {
1574
1578
  const lm = localManifest(e.agent_id) || {};
@@ -1606,7 +1610,7 @@ const server = http.createServer(async (req, res) => {
1606
1610
  saveRegistry(reg);
1607
1611
  }
1608
1612
  }
1609
- return json(res, 200, { online, last_sync_at: reg.last_sync_at || null, agents: [...byId.values()] });
1613
+ return json(res, 200, { online, auth_error: authError, last_sync_at: reg.last_sync_at || null, agents: [...byId.values()] });
1610
1614
  }
1611
1615
  // ── 新建助手:本地起草(AI 生成 prompt)→ ARM JSON 创建(创建即发布)→ 可选知识绑定(幂等)→ 下载物化 ──
1612
1616
  if (req.method === 'POST' && pathname === '/api/agents/draft-prompt') {
@@ -1614,8 +1618,8 @@ const server = http.createServer(async (req, res) => {
1614
1618
  if (!String(description || '').trim()) return json(res, 400, { error: '请先填写助手职责描述' });
1615
1619
  try {
1616
1620
  const ctx = {
1617
- systemPrompt: '你是助手档案起草器。根据用户对助手职责的描述写出可直接使用的中文 System Prompt:第一句角色定位;随后 3-6 条职责要点(每条一个短句);最后 1-2 条工作方式或能力边界约束。只输出 Prompt 正文,不要标题、不要解释,200 字以内。',
1618
- messages: [{ role: 'user', content: `助手名称:${String(name || '').trim() || '(待定)'}\n职责描述:${String(description).trim()}\n\n请生成 System Prompt:`, timestamp: Date.now() }],
1621
+ systemPrompt: '你是助手档案起草器。根据用户的职责描述输出严格 JSON(不要 markdown 代码围栏):{"name":"助手名称","prompt":"System Prompt"}。name:12 字以内的中文名称,体现职责;prompt:第一句角色定位,随后 3-6 条职责要点(每条一个短句),最后 1-2 条工作方式或能力边界约束,200 字以内。只输出 JSON。',
1622
+ messages: [{ role: 'user', content: `助手名称(用户意向,可参考):${String(name || '').trim() || '(待定)'}\n职责描述:${String(description).trim()}\n\n请输出 JSON:`, timestamp: Date.now() }],
1619
1623
  tools: [],
1620
1624
  };
1621
1625
  const stream = models.stream(model, ctx);
@@ -1623,7 +1627,10 @@ const server = http.createServer(async (req, res) => {
1623
1627
  for await (const ev of stream) { if (ev.type === 'text_delta') out += ev.delta; }
1624
1628
  const final = await stream.result();
1625
1629
  const text = ((final.content || []).filter(b => b.type === 'text').map(b => b.text).join('') || out).trim();
1626
- return json(res, 200, { prompt: text });
1630
+ let draftName = '', prompt = text;
1631
+ const jm = text.match(/\{[\s\S]*\}/); // JSON 容错解析:失败则整段当 prompt,不阻断
1632
+ if (jm) { try { const j = JSON.parse(jm[0]); if (j.prompt) { prompt = String(j.prompt).trim(); if (j.name) draftName = String(j.name).trim().slice(0, 24); } } catch { } }
1633
+ return json(res, 200, { prompt, name: draftName });
1627
1634
  } catch (e) { return json(res, 502, { error: `起草失败: ${e.message}` }); }
1628
1635
  }
1629
1636
  if (req.method === 'GET' && pathname === '/api/agents/knowledges') { // 新建向导的知识选择器:ARM 关键词检索