aims-workstation 0.1.13 → 0.1.15
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/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.15",
|
|
17
17
|
"description": "AIMS 工作站:本地智能体工作站——ARM 助手本地化(下载/离线/升级)、工作台对话式定制、多工作区隔离、分级自主与审计",
|
|
18
18
|
"bin": {
|
|
19
19
|
"aims-workstation": "bin/aims-workstation.js"
|
package/server.js
CHANGED
|
@@ -272,7 +272,21 @@ async function armListAgents(keyword = '') {
|
|
|
272
272
|
out.push(...(d.agents || []));
|
|
273
273
|
if (!out.length || out.length >= +(d.total || 0)) break;
|
|
274
274
|
}
|
|
275
|
-
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
|
+
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_id: a.createdBy || '', created_by: a.creatorName || a.createdBy || '' }));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// PAT 身份(带缓存):身份只随 (ARM 源, 令牌) 变化——key 命中直接复用,避免目录高频刷新
|
|
279
|
+
// (搜索防抖每 400ms 一次 /api/agents)时每次都打 /auth/me。失败不缓存:未配置令牌/不可达时
|
|
280
|
+
// 返回 null,恢复后下次自动重试。设置页改源/换令牌 → key 变化 → 自动重新获取。
|
|
281
|
+
const armMeCache = { key: '', me: null };
|
|
282
|
+
async function armMe() {
|
|
283
|
+
const key = `${armApiBase()}|${armApiToken()}`;
|
|
284
|
+
if (armMeCache.key === key && armMeCache.me) return armMeCache.me;
|
|
285
|
+
try {
|
|
286
|
+
const u = await armRest('/api/v1/auth/me');
|
|
287
|
+
if (u && u.id) { armMeCache.key = key; armMeCache.me = { id: u.id, name: u.name || '', role: u.role || 'USER' }; return armMeCache.me; }
|
|
288
|
+
} catch { }
|
|
289
|
+
return null;
|
|
276
290
|
}
|
|
277
291
|
|
|
278
292
|
// 知识检索(全局经验库):REST 混合检索(RAG 语义 ⊕ 关键词);不可达由调用方降级(本地知识关键词命中)
|
|
@@ -382,6 +396,27 @@ async function downloadAgent(agentId) {
|
|
|
382
396
|
return manifest;
|
|
383
397
|
}
|
|
384
398
|
|
|
399
|
+
// 本地档案写入(离线创建 / 未发布档案编辑):与 downloadAgent 同构的物化结构,source 标记 local。
|
|
400
|
+
// 发布到 ARM(同 id,见 /publish)后即转正为远端档案,本地结构不变——身份全程稳定。
|
|
401
|
+
const bumpPatch = (v) => { const p = String(v || 'v1.0.0').replace(/^v/, '').split('.').map(n => +n || 0); p[2] = (p[2] || 0) + 1; return `v${p[0] || 1}.${p[1] || 0}.${p[2]}`; };
|
|
402
|
+
function writeLocalAgent(agentId, { name, description, prompt }, { bump = false } = {}) {
|
|
403
|
+
const cur = bump ? (localManifest(agentId) || {}) : {};
|
|
404
|
+
const version = bump ? bumpPatch(cur.version) : 'v1.0.0';
|
|
405
|
+
const verDir = path.join(ARM_AGENTS_DIR, agentId, 'versions', version);
|
|
406
|
+
fs.mkdirSync(path.join(verDir, 'knowledges'), { recursive: true });
|
|
407
|
+
const manifest = {
|
|
408
|
+
agent_id: agentId, name, version, description: String(description || '').slice(0, 512),
|
|
409
|
+
downloaded_at: new Date().toISOString(), source: 'local',
|
|
410
|
+
prompt: String(prompt || ''),
|
|
411
|
+
skills: cur.skills || [], knowledges: cur.knowledges || [], ui_bindings: cur.ui_bindings || [], // 未发布档案无 ARM 绑定;编辑时保留既有
|
|
412
|
+
};
|
|
413
|
+
fs.writeFileSync(path.join(verDir, 'prompt.md'), manifest.prompt);
|
|
414
|
+
fs.writeFileSync(path.join(verDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
415
|
+
commitDownload(manifest);
|
|
416
|
+
audit({ actor: 'user', agentId, action: bump ? 'update-local-agent' : 'create-local-agent', version });
|
|
417
|
+
return manifest;
|
|
418
|
+
}
|
|
419
|
+
|
|
385
420
|
// 同步:比对远端目录与本地库,产出 new/update/delisted 事件;顺带冲账离线经验 outbox
|
|
386
421
|
async function armSync() {
|
|
387
422
|
const remote = await armListAgents();
|
|
@@ -1664,6 +1699,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1664
1699
|
// ── 静态前端(web/dist 生产构建)──
|
|
1665
1700
|
if (req.method === 'GET' && !pathname.startsWith('/api')) {
|
|
1666
1701
|
let p = path.join(ROOT, 'web/dist', pathname === '/' ? 'index.html' : pathname);
|
|
1702
|
+
// 目录路径解析为其 index.html(否则 readFileSync 读目录返回空响应体)
|
|
1703
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) p = path.join(p, 'index.html');
|
|
1667
1704
|
if (!p.startsWith(path.join(ROOT, 'web/dist')) || !fs.existsSync(p)) p = path.join(ROOT, 'web/dist/index.html');
|
|
1668
1705
|
if (fs.existsSync(p)) {
|
|
1669
1706
|
const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon' }[path.extname(p)] || 'application/octet-stream';
|
|
@@ -1825,6 +1862,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1825
1862
|
installed_version: e.installed_version, remote_version: e.remote_version || null,
|
|
1826
1863
|
update_available: !!(e.remote_version && cmpVersion(e.remote_version, e.installed_version) > 0),
|
|
1827
1864
|
delisted: !!e.delisted, downloaded_at: e.downloaded_at || null,
|
|
1865
|
+
created_by_id: null, created_by: null, // 属主身份以远端目录为准(离线/已下架不可编辑,无需属主)
|
|
1866
|
+
local_only: !!e.local_only, // 本地未发布档案(离线创建):大厅显示「未发布」徽章 + 发布入口
|
|
1828
1867
|
});
|
|
1829
1868
|
}
|
|
1830
1869
|
if (online) { // 静默刷新远端版本与下架标记(显式事件统计走 /api/arm/sync)——仅在完整拉取时;
|
|
@@ -1837,6 +1876,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1837
1876
|
description: c.description || cur.description, remote_version: c.version,
|
|
1838
1877
|
deployed: !!loadDeployment(c.agent_id),
|
|
1839
1878
|
update_available: cur.installed ? cmpVersion(c.version, cur.installed_version) > 0 : false,
|
|
1879
|
+
created_by_id: c.created_by_id || null, created_by: c.created_by || null,
|
|
1840
1880
|
});
|
|
1841
1881
|
}
|
|
1842
1882
|
if (!kw) {
|
|
@@ -1848,7 +1888,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1848
1888
|
saveRegistry(reg);
|
|
1849
1889
|
}
|
|
1850
1890
|
}
|
|
1851
|
-
|
|
1891
|
+
// PAT 身份(编辑/删除门控依据:me.id vs 卡片 created_by_id,或 ADMIN):armMe 有缓存——
|
|
1892
|
+
// 身份只随 (ARM 源, 令牌) 变化,搜索防抖高频刷新目录时不重复打 /auth/me;失败降级 null 不阻断目录
|
|
1893
|
+
const me = online ? await armMe() : null;
|
|
1894
|
+
return json(res, 200, { online, auth_error: authError, me, last_sync_at: reg.last_sync_at || null, agents: [...byId.values()] });
|
|
1852
1895
|
}
|
|
1853
1896
|
// ── 新建助手:本地起草(AI 生成 prompt)→ ARM JSON 创建(创建即发布)→ 可选知识绑定(幂等)→ 下载物化 ──
|
|
1854
1897
|
if (req.method === 'POST' && pathname === '/api/agents/draft-prompt') {
|
|
@@ -1887,24 +1930,36 @@ const server = http.createServer(async (req, res) => {
|
|
|
1887
1930
|
const { name, description, prompt, knowledge_ids } = await body(req);
|
|
1888
1931
|
const nm = String(name || '').trim(), pr = String(prompt || '').trim();
|
|
1889
1932
|
if (!nm || !pr) return json(res, 400, { error: '名称与 System Prompt 必填' });
|
|
1933
|
+
// 身份本地生成并随创建上行(新版 ARM 保持同 id;旧版忽略后以其返回 id 为准,行为不变)
|
|
1934
|
+
const localId = crypto.randomUUID();
|
|
1935
|
+
let newId, bound = 0, local = false;
|
|
1890
1936
|
try {
|
|
1891
|
-
const created = await armRest('/api/v1/agents', { method: 'POST', body: JSON.stringify({ name: nm, description: String(description || '').trim().slice(0, 512), prompt: pr, status: 'active' }) });
|
|
1892
|
-
|
|
1937
|
+
const created = await armRest('/api/v1/agents', { method: 'POST', body: JSON.stringify({ id: localId, name: nm, description: String(description || '').trim().slice(0, 512), prompt: pr, status: 'active' }) });
|
|
1938
|
+
newId = created?.id || created?.agent?.id;
|
|
1893
1939
|
if (!newId) throw new Error('ARM 未返回新助手 id');
|
|
1894
|
-
let bound = 0;
|
|
1895
1940
|
for (const kid of (Array.isArray(knowledge_ids) ? knowledge_ids : [])) { // 绑定幂等(重复绑定返回已有版本)
|
|
1896
1941
|
try { await armRest(`/api/v1/agents/${newId}/knowledges`, { method: 'POST', body: JSON.stringify({ knowledgeId: kid, kind: 'experience' }) }); bound++; }
|
|
1897
1942
|
catch { /* 单条绑定失败不阻断创建 */ }
|
|
1898
1943
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
return json(res,
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
// 离线降级:仅网络不可达(ARM 请求失败/超时)时本地创建(07 离线理念:出生不依赖 ARM);
|
|
1946
|
+
// 业务错误(如名称已存在)照常上抛——那种情况下静默转本地只会掩盖问题。
|
|
1947
|
+
if (!/^ARM 请求失败/.test(String(e.message))) return json(res, 502, { error: `创建失败: ${e.message}` });
|
|
1948
|
+
newId = localId; local = true;
|
|
1949
|
+
}
|
|
1950
|
+
const manifest = local
|
|
1951
|
+
? writeLocalAgent(newId, { name: nm, description: String(description || '').trim(), prompt: pr })
|
|
1952
|
+
: await downloadAgent(newId); // 创建即物化本地(沿用下载链路)
|
|
1953
|
+
const reg = loadRegistry(); const entry = reg.agents.find((x) => x.agent_id === newId);
|
|
1954
|
+
if (entry) { entry.created_via_workstation = true; if (local) entry.local_only = true; } saveRegistry(reg);
|
|
1955
|
+
audit({ actor: 'user', action: local ? 'create-agent-local' : 'create-agent', agentId: newId, name: nm, knowledges: bound });
|
|
1956
|
+
return json(res, 200, { agent_id: newId, manifest, knowledges_bound: bound, local });
|
|
1957
|
+
}
|
|
1958
|
+
// 编辑档案(工作站创建的助手):ARM PUT 后重新物化,本地 prompt 同步更新;未发布档案仅写本地(版本前进)
|
|
1906
1959
|
if (req.method === 'GET' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/profile$/)) {
|
|
1907
1960
|
const id = pathname.split('/')[3];
|
|
1961
|
+
const le = loadRegistry().agents.find((x) => x.agent_id === id);
|
|
1962
|
+
if (le && le.local_only) { const m = localManifest(id) || {}; return json(res, 200, { name: m.name || le.name, description: String(m.description || ''), prompt: m.prompt || '' }); }
|
|
1908
1963
|
try {
|
|
1909
1964
|
const d = await armRest(`/api/v1/agents/${id}`);
|
|
1910
1965
|
return json(res, 200, { name: d.name, description: String(d.description || ''), prompt: d.prompt || '' });
|
|
@@ -1918,6 +1973,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1918
1973
|
if (description !== undefined) patch.description = String(description || '').trim().slice(0, 512);
|
|
1919
1974
|
if (String(prompt || '').trim()) patch.prompt = String(prompt).trim();
|
|
1920
1975
|
if (!Object.keys(patch).length) return json(res, 400, { error: '没有可更新字段' });
|
|
1976
|
+
const le = loadRegistry().agents.find((x) => x.agent_id === id);
|
|
1977
|
+
if (le && le.local_only) { // 未发布档案:仅写本地(版本前进可回滚),发布后编辑将同步 ARM
|
|
1978
|
+
const cur = localManifest(id) || {};
|
|
1979
|
+
const manifest = writeLocalAgent(id, { name: patch.name || cur.name, description: patch.description ?? cur.description, prompt: patch.prompt || cur.prompt }, { bump: true });
|
|
1980
|
+
return json(res, 200, { ok: true, manifest, local: true });
|
|
1981
|
+
}
|
|
1921
1982
|
try {
|
|
1922
1983
|
await armRest(`/api/v1/agents/${id}`, { method: 'PUT', body: JSON.stringify(patch) });
|
|
1923
1984
|
let manifest = null;
|
|
@@ -1926,18 +1987,114 @@ const server = http.createServer(async (req, res) => {
|
|
|
1926
1987
|
return json(res, 200, { ok: true, manifest });
|
|
1927
1988
|
} catch (e) { return json(res, 502, { error: `更新失败: ${e.message}` }); }
|
|
1928
1989
|
}
|
|
1929
|
-
//
|
|
1990
|
+
// ── 绑定管理(编辑档案对话框):skill/知识绑定增删代理 ARM,变更后重新物化本地(版本前进可回滚)──
|
|
1991
|
+
if (req.method === 'GET' && pathname === '/api/agents/skills') { // 绑定选择器的技能检索(对应知识检索 /api/agents/knowledges)
|
|
1992
|
+
const kwd = url.searchParams.get('keyword') || '';
|
|
1993
|
+
try {
|
|
1994
|
+
const d = await armRest(`/api/v1/skills?keyword=${encodeURIComponent(kwd)}&page=1&pageSize=20`);
|
|
1995
|
+
return json(res, 200, { items: (d.skills || []).map((s) => ({ id: s.id, name: s.name, description: String(s.description || '').slice(0, 80) })) });
|
|
1996
|
+
} catch (e) { return json(res, 200, { items: [], error: String(e.message).slice(0, 100) }); }
|
|
1997
|
+
}
|
|
1998
|
+
let mBind = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/bindings$/);
|
|
1999
|
+
if (mBind && req.method === 'GET') {
|
|
2000
|
+
const le = loadRegistry().agents.find((x) => x.agent_id === mBind[1]);
|
|
2001
|
+
if (le && le.local_only) return json(res, 200, { skills: [], knowledges: [], local_only: true }); // 未发布档案无 ARM 绑定
|
|
2002
|
+
try {
|
|
2003
|
+
const d = await armRest(`/api/v1/agents/${mBind[1]}`);
|
|
2004
|
+
return json(res, 200, {
|
|
2005
|
+
skills: (d.skills || []).map((b) => ({ binding_id: b.id, ref_id: b.skillId, name: b.skill?.name || b.skillId, description: String(b.skill?.description || '').slice(0, 80) })),
|
|
2006
|
+
knowledges: (d.knowledges || []).map((b) => ({ binding_id: b.id, ref_id: b.knowledgeId, name: b.knowledge?.name || b.knowledgeId, description: String(b.knowledge?.description || '').slice(0, 80), kind: b.kind })),
|
|
2007
|
+
});
|
|
2008
|
+
} catch (e) { return json(res, 502, { error: `绑定读取失败: ${e.message}` }); }
|
|
2009
|
+
}
|
|
2010
|
+
if (mBind && req.method === 'POST') {
|
|
2011
|
+
const { type, ref_id, kind } = await body(req);
|
|
2012
|
+
if (!['skill', 'knowledge'].includes(type) || !ref_id) return json(res, 400, { error: 'type 须为 skill|knowledge,且提供 ref_id' });
|
|
2013
|
+
const sub = type === 'skill' ? 'skills' : 'knowledges';
|
|
2014
|
+
const payload = type === 'skill' ? { skillId: ref_id } : { knowledgeId: ref_id, kind: kind === 'experience' ? 'experience' : 'essential' };
|
|
2015
|
+
try {
|
|
2016
|
+
const r = await armRest(`/api/v1/agents/${mBind[1]}/${sub}`, { method: 'POST', body: JSON.stringify(payload) });
|
|
2017
|
+
let manifest = null;
|
|
2018
|
+
try { manifest = await downloadAgent(mBind[1]); } catch { /* 物化失败不回滚 ARM 变更,提示用户重新下载 */ }
|
|
2019
|
+
audit({ actor: 'user', action: 'bind-agent-asset', agentId: mBind[1], type, ref_id, version: r?.version });
|
|
2020
|
+
return json(res, 200, { ok: true, version: r?.version || null, manifest });
|
|
2021
|
+
} catch (e) { return json(res, 502, { error: `绑定失败: ${e.message}` }); }
|
|
2022
|
+
}
|
|
2023
|
+
if (mBind && req.method === 'DELETE') {
|
|
2024
|
+
const { type, binding_id } = await body(req);
|
|
2025
|
+
if (!['skill', 'knowledge'].includes(type) || !binding_id) return json(res, 400, { error: 'type 须为 skill|knowledge,且提供 binding_id' });
|
|
2026
|
+
try {
|
|
2027
|
+
await armRest(`/api/v1/agents/${mBind[1]}/${type === 'skill' ? 'skills' : 'knowledges'}?bindingId=${encodeURIComponent(binding_id)}`, { method: 'DELETE' });
|
|
2028
|
+
let manifest = null;
|
|
2029
|
+
try { manifest = await downloadAgent(mBind[1]); } catch { }
|
|
2030
|
+
audit({ actor: 'user', action: 'unbind-agent-asset', agentId: mBind[1], type, binding_id });
|
|
2031
|
+
return json(res, 200, { ok: true, manifest });
|
|
2032
|
+
} catch (e) { return json(res, 502, { error: `解绑失败: ${e.message}` }); }
|
|
2033
|
+
}
|
|
2034
|
+
// 克隆助手(fork):复制档案 + 全部绑定,创建归当前 PAT 用户的新助手——非属主想改 / 想基于
|
|
2035
|
+
// 已有快速调教的正路。无需对源助手的编辑权限(源详情是公开读);新助手属主即当前用户,
|
|
2036
|
+
// 后续绑定守卫天然放行。绑定逐条 best-effort(单条失败计数不阻断),UI 资产跟随最新版本。
|
|
2037
|
+
if (req.method === 'POST' && pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/clone$/)) {
|
|
2038
|
+
const id = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/clone$/)[1];
|
|
2039
|
+
const { name, description } = await body(req);
|
|
2040
|
+
try {
|
|
2041
|
+
const src = await armRest(`/api/v1/agents/${id}`);
|
|
2042
|
+
const nm = String(name || `${src.name}-副本`).trim();
|
|
2043
|
+
const created = await armRest('/api/v1/agents', { method: 'POST', body: JSON.stringify({ name: nm, description: String(description ?? src.description ?? '').slice(0, 512), prompt: src.prompt || '', status: 'active' }) });
|
|
2044
|
+
const newId = created?.id || created?.agent?.id;
|
|
2045
|
+
if (!newId) throw new Error('ARM 未返回新助手 id');
|
|
2046
|
+
const copied = { skills: 0, knowledges: 0, ui_assets: 0, failed: 0 };
|
|
2047
|
+
for (const b of (src.skills || [])) { try { await armRest(`/api/v1/agents/${newId}/skills`, { method: 'POST', body: JSON.stringify({ skillId: b.skillId }) }); copied.skills++; } catch { copied.failed++; } }
|
|
2048
|
+
for (const b of (src.knowledges || [])) { try { await armRest(`/api/v1/agents/${newId}/knowledges`, { method: 'POST', body: JSON.stringify({ knowledgeId: b.knowledgeId, kind: b.kind || 'experience' }) }); copied.knowledges++; } catch { copied.failed++; } }
|
|
2049
|
+
for (const b of (src.uiBindings || [])) { try { await armRest(`/api/v1/agents/${newId}/ui-assets`, { method: 'POST', body: JSON.stringify({ name: b.asset?.name || b.uiAssetId }) }); copied.ui_assets++; } catch { copied.failed++; } }
|
|
2050
|
+
const manifest = await downloadAgent(newId); // 创建即物化(沿用新建链路)
|
|
2051
|
+
const reg = loadRegistry(); const entry = reg.agents.find((x) => x.agent_id === newId); if (entry) entry.created_via_workstation = true; saveRegistry(reg);
|
|
2052
|
+
audit({ actor: 'user', action: 'clone-agent', agentId: newId, source: id, name: nm, ...copied });
|
|
2053
|
+
return json(res, 200, { agent_id: newId, name: nm, copied, manifest: { agent_id: manifest.agent_id, name: manifest.name, version: manifest.version } });
|
|
2054
|
+
} catch (e) { return json(res, 502, { error: `克隆失败: ${e.message}` }); }
|
|
2055
|
+
}
|
|
2056
|
+
// 从 ARM 删除(危险操作,前端二次确认):可选同时卸载本地档案;未发布档案只存在于本地,直接卸载
|
|
1930
2057
|
if (req.method === 'POST' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/arm-delete$/)) {
|
|
1931
2058
|
const id = pathname.split('/')[3];
|
|
1932
2059
|
const { also_uninstall, purge_workspaces } = await body(req);
|
|
1933
2060
|
try {
|
|
1934
|
-
|
|
2061
|
+
const le = loadRegistry().agents.find((x) => x.agent_id === id);
|
|
2062
|
+
if (!(le && le.local_only)) await armRest(`/api/v1/agents/${id}`, { method: 'DELETE' });
|
|
1935
2063
|
let uninstalled = false;
|
|
1936
|
-
if (also_uninstall &&
|
|
1937
|
-
audit({ actor: 'user', action: 'arm-delete-agent', agentId: id, also_uninstalled: uninstalled });
|
|
2064
|
+
if (also_uninstall && le) uninstalled = uninstallLocalAgent(id, !!purge_workspaces);
|
|
2065
|
+
audit({ actor: 'user', action: le && le.local_only ? 'delete-local-agent' : 'arm-delete-agent', agentId: id, also_uninstalled: uninstalled });
|
|
1938
2066
|
return json(res, 200, { ok: true, uninstalled });
|
|
1939
2067
|
} catch (e) { return json(res, 502, { error: `ARM 删除失败: ${e.message}` }); }
|
|
1940
2068
|
}
|
|
2069
|
+
// 发布到 ARM(本地未发布档案转正):同 id 上行(新版 ARM),身份不变、本地 deployment/工作台/数据零迁移。
|
|
2070
|
+
// 幂等:同 id 已在 ARM(上次发布部分成功)→ 校验同名即续走转正;旧版 ARM 忽略 id 返回新 id → 明确报错提示升级。
|
|
2071
|
+
if (req.method === 'POST' && pathname.match(/^\/api\/agents\/[0-9a-f-]{36}\/publish$/)) {
|
|
2072
|
+
const id = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/publish$/)[1];
|
|
2073
|
+
const reg = loadRegistry();
|
|
2074
|
+
const le = reg.agents.find((x) => x.agent_id === id);
|
|
2075
|
+
if (!le || !le.local_only) return json(res, 400, { error: '该助手不是本地未发布档案(可能已发布)' });
|
|
2076
|
+
const m = localManifest(id);
|
|
2077
|
+
if (!m) return json(res, 400, { error: '本地档案缺失,请先重新创建' });
|
|
2078
|
+
try {
|
|
2079
|
+
let created;
|
|
2080
|
+
try {
|
|
2081
|
+
created = await armRest('/api/v1/agents', { method: 'POST', body: JSON.stringify({ id, name: m.name, description: String(m.description || '').slice(0, 512), prompt: m.prompt || '', status: 'active' }) });
|
|
2082
|
+
} catch (e) {
|
|
2083
|
+
if (/该 id 已存在/.test(String(e.message))) { // 幂等续走:确认同 id 同名即视为已创建
|
|
2084
|
+
const d = await armRest(`/api/v1/agents/${id}`);
|
|
2085
|
+
if (!d || d.name !== m.name) throw e;
|
|
2086
|
+
created = d;
|
|
2087
|
+
} else throw e;
|
|
2088
|
+
}
|
|
2089
|
+
const finalId = created?.id || created?.agent?.id;
|
|
2090
|
+
if (finalId && finalId !== id) return json(res, 502, { error: '当前 ARM 版本不支持保持助手身份(忽略客户端 id)——请升级 ARM 后再发布' });
|
|
2091
|
+
const manifest = await downloadAgent(id); // 重新物化:source 转正、版本对齐 ARM
|
|
2092
|
+
const reg2 = loadRegistry(); const en = reg2.agents.find((x) => x.agent_id === id);
|
|
2093
|
+
if (en) { en.local_only = false; en.remote_version = manifest.version; } saveRegistry(reg2);
|
|
2094
|
+
audit({ actor: 'user', action: 'publish-agent', agentId: id, name: m.name, version: manifest.version });
|
|
2095
|
+
return json(res, 200, { ok: true, manifest });
|
|
2096
|
+
} catch (e) { return json(res, 502, { error: `发布失败: ${e.message}` }); }
|
|
2097
|
+
}
|
|
1941
2098
|
// ── 本地助手库:下载 / 升级 / 版本历史 / 回滚 / 卸载 ──
|
|
1942
2099
|
let mAgt = pathname.match(/^\/api\/agents\/([0-9a-f-]{36})\/(download|upgrade|versions|rollback)$/);
|
|
1943
2100
|
if (mAgt && req.method === 'POST' && (mAgt[2] === 'download' || mAgt[2] === 'upgrade')) {
|
|
@@ -2379,14 +2536,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
2379
2536
|
dsl.arm_ref = armRef;
|
|
2380
2537
|
fs.writeFileSync(target, JSON.stringify(dsl, null, 2));
|
|
2381
2538
|
}
|
|
2382
|
-
let bound = null;
|
|
2383
|
-
if (bind_agent) { //
|
|
2384
|
-
|
|
2385
|
-
|
|
2539
|
+
let bound = null, bind_error = null;
|
|
2540
|
+
if (bind_agent) { // 发布后顺手绑定到当前助手(三资产组合:员工自带工位);失败不回滚发布(如非属主 403:仅助手创建者或管理员可绑定)
|
|
2541
|
+
try {
|
|
2542
|
+
const r = await armRest(`/api/v1/agents/${bind_agent}/ui-assets`, { method: 'POST', body: JSON.stringify({ name: bundle.name }) });
|
|
2543
|
+
bound = r && r.version;
|
|
2544
|
+
} catch (e) { bind_error = String(e.message).slice(0, 200); }
|
|
2386
2545
|
}
|
|
2387
2546
|
metric({ kind: 'asset_share', type, name: bundle.name, version: uploaded.version });
|
|
2388
|
-
audit({ actor: 'asset-share', target: 'ARM', type, name: bundle.name, version: uploaded.version, bound });
|
|
2389
|
-
return json(res, 200, { name: bundle.name, version: uploaded.version, requires: bundle.manifest.requires, bound });
|
|
2547
|
+
audit({ actor: 'asset-share', target: 'ARM', type, name: bundle.name, version: uploaded.version, bound, bind_error });
|
|
2548
|
+
return json(res, 200, { name: bundle.name, version: uploaded.version, requires: bundle.manifest.requires, bound, bind_error });
|
|
2390
2549
|
} catch (e) { return json(res, 400, { error: String(e.message).slice(0, 400) }); }
|
|
2391
2550
|
}
|
|
2392
2551
|
if (req.method === 'GET' && pathname === '/api/assets/arm-library') {
|