@sanqianx/claude-ai-workbench 0.12.0 → 0.13.1
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 +10 -0
- package/apps/agent-terminal/app.css +34 -0
- package/apps/agent-terminal/app.js +120 -25
- package/apps/agent-terminal/index.html +4 -3
- package/docs/sdk/openapi.yaml +1 -1
- package/package.json +1 -1
- package/packages/client/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/core/package.json +2 -2
- package/packages/server/bin/agent-terminal-server.js +6 -1
- package/packages/server/index.js +30 -9
- package/packages/server/lib/agent-discovery.js +10 -7
- package/packages/server/lib/capture-settings-store.js +2 -1
- package/packages/server/lib/conversation-capture.js +50 -0
- package/packages/server/lib/diagnostics.js +32 -0
- package/packages/server/lib/drivers/cli-json-runner.js +1 -1
- package/packages/server/lib/drivers/codex-driver.js +1 -1
- package/packages/server/lib/drivers/opencode-driver.js +1 -1
- package/packages/server/lib/drivers/zcode-driver.js +1 -1
- package/packages/server/lib/http-handler.js +30 -0
- package/packages/server/lib/knowledge-context.js +42 -0
- package/packages/server/lib/profile-store.js +130 -0
- package/packages/server/lib/system-doctor.js +4 -2
- package/packages/server/lib/workbench-service.js +55 -8
- package/packages/server/package.json +2 -2
- package/packages/ui/package.json +2 -2
- package/src/backend/lib/claude-cli-runner.js +42 -32
package/README.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
0.3.0 起,仓库同时提供 **Agent Terminal**:一个通用多 Agent 对话终端(Claude Code / Codex / OpenCode / ZCode),带多项目工作区、自定义模型供应商管理、Agent 发现与能力矩阵、推理力度与上下文用量显示。
|
|
6
6
|
|
|
7
|
+
## 作为智脑产品的嵌入模块
|
|
8
|
+
|
|
9
|
+
外壳使用真实 Agent Terminal 页面和 REST/SSE,不重新实现消息流。`embed=1` 与可配置 `apiBase` 隐藏重复品牌/项目管理,并接受同源父窗口的项目、主题与知识范围消息;独立产品行为保持不变。
|
|
10
|
+
|
|
11
|
+
受信任本地外壳通过 `KB_WORKBENCH_RUNTIME_TOKEN`、`KB_EVENT_BRIDGE_URL` 和引擎地址连接后台服务。原生工作台捕获原始用户输入及成功最终回答,通过持久化 outbox 投递到 bridge 的鉴权入口;不捕获系统提示词、注入的知识、工具输出或供应商凭据。`/api/health` 中 `conversationCapture` 展示启用状态、待投递数和错误;已有用户显式关闭的捕获设置不会被覆盖。桥接不可用时保留待投递记录,恢复或重启后继续,不伪造会话。
|
|
12
|
+
|
|
13
|
+
工作台是产品供应商与凭据的活动配置源。后台维护默认模型持久化,不随会话临时模型切换;解析后的凭据只交给鉴权本地服务,不进入浏览器消息。SDK 会话从当前项目登记的工作区恢复,也兼容旧登记文件。
|
|
14
|
+
|
|
15
|
+
Windows 上 Claude Code 探测与实际启动使用同一解析结果:失效的 `CLAUDE_CODE_EXECPATH` 不遮住有效安装,npm/CMD/PowerShell 启动脚本解析成实际文件,JavaScript 入口用当前 Node 探测版本。`/system/doctor` 返回实际启动路径、被跳过的失效路径及版本探测错误。发现和会话失败使用 `log/v2`;受外壳守护时写入外壳统一诊断,独立运行保存在工作台数据目录的 `diagnostics.jsonl`,不记录凭据或原始提示词。
|
|
16
|
+
|
|
7
17
|
## 仓库结构
|
|
8
18
|
|
|
9
19
|
- `packages/`:可发布的 SDK 源码。
|
|
@@ -565,3 +565,37 @@ body.light .msg.ai.error { color:#b42318; border-color:rgba(180,35,24,.25); back
|
|
|
565
565
|
|
|
566
566
|
/* ===== embed 模式(被知识库壳嵌入):折叠左侧项目栏,由壳 postMessage 控制 ===== */
|
|
567
567
|
body.embed-collapse .sidebar { display: none; }
|
|
568
|
+
|
|
569
|
+
/* Embedded product tokens are owned by the shell; standalone styling stays. */
|
|
570
|
+
body.kb-embedded.light {
|
|
571
|
+
--page:#ffffff; --side:#f8f9fb; --surface:#f3f5f7; --surface2:#edf1f4;
|
|
572
|
+
--line:#dce2e8; --line2:#dce2e8; --pop:#ffffff;
|
|
573
|
+
--text:#202733; --muted:#687587; --faint:#8e99a7;
|
|
574
|
+
--good:#397761; --bad:#a05252; --blue:#516f89; --blue-soft:#e6edf3;
|
|
575
|
+
--code-bg:#f3f5f7; --code-line:#dce2e8;
|
|
576
|
+
}
|
|
577
|
+
body.kb-embedded:not(.light) {
|
|
578
|
+
--page:#20252d; --side:#1b2028; --surface:#252b34; --surface2:#2c333e;
|
|
579
|
+
--line:#39424f; --line2:#39424f; --pop:#252b34;
|
|
580
|
+
--text:#e6ebf0; --muted:#a0aaba; --faint:#788393; --blue:#8aa8cf; --blue-soft:#293748;
|
|
581
|
+
--good:#81b29a; --bad:#d88b8b; --code-bg:#252b34; --code-line:#39424f;
|
|
582
|
+
}
|
|
583
|
+
body.kb-embedded { --accent:var(--blue); --accent-soft:var(--blue-soft); }
|
|
584
|
+
body.kb-embedded .agent-badge { background:var(--blue); }
|
|
585
|
+
body.kb-embedded .sidebar { display:flex; width:208px; min-width:208px; border-right:1px solid var(--line); }
|
|
586
|
+
body.kb-embedded .brand, body.kb-embedded .group-label, body.kb-embedded .add-proj,
|
|
587
|
+
body.kb-embedded .proj-item .row-acts, body.kb-embedded .sidebar-foot, body.kb-embedded #btn-top-settings { display:none; }
|
|
588
|
+
body.kb-embedded .new-session { margin:16px 16px 8px; }
|
|
589
|
+
body.kb-embedded .hint-spark, body.kb-embedded .hint-art { display:none; }
|
|
590
|
+
body.kb-embedded .empty-hint .big { display:none; }
|
|
591
|
+
.embedded-hint { display:none; }
|
|
592
|
+
body.kb-embedded .standalone-hint { display:none; }
|
|
593
|
+
body.kb-embedded .embedded-hint { display:block; }
|
|
594
|
+
body.kb-embedded .cmdrow { min-width:0; }
|
|
595
|
+
body.kb-embedded .cmdrow > .bar-btn, body.kb-embedded .send { flex-shrink:0; }
|
|
596
|
+
body.kb-embedded .cmdrow > .anchor { flex:0 0 auto; }
|
|
597
|
+
body.kb-embedded .cmdrow > .model-anchor { flex:0 1 auto; min-width:0; }
|
|
598
|
+
body.kb-embedded #btn-model { min-width:0; max-width:100%; }
|
|
599
|
+
body.kb-embedded #model-label { display:block; min-width:0; max-width:100%; }
|
|
600
|
+
body.kb-embedded #btn-ctx .ctx-mini { display:none; }
|
|
601
|
+
body.kb-embedded .ctx-mini i { background:var(--blue); }
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
/* Agent Terminal — full-page UI over the versioned REST/SSE API. */
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const embedParams = new URLSearchParams(location.search);
|
|
5
|
+
const embedded = embedParams.get('embed') === '1';
|
|
6
|
+
const configuredApiBase = embedParams.get('apiBase');
|
|
7
|
+
const apiBase = configuredApiBase && /^\/api\/[a-z0-9/-]+$/i.test(configuredApiBase) ? configuredApiBase.replace(/\/$/, '') : '/api/claude-workbench/v1';
|
|
8
|
+
let selectionEpoch = 0;
|
|
9
|
+
let sessionEpoch = 0;
|
|
10
|
+
let sessionCreationEpoch = 0;
|
|
11
|
+
const projectDrafts = new Map();
|
|
12
|
+
function savedProjectDraft(projectId, text) {
|
|
13
|
+
try {
|
|
14
|
+
const key = `kb.workbench.drafts:${apiBase}`;
|
|
15
|
+
const drafts = JSON.parse(localStorage.getItem(key) || '{}');
|
|
16
|
+
if (text !== undefined) {
|
|
17
|
+
delete drafts[projectId];
|
|
18
|
+
if (text) drafts[projectId] = String(text).slice(0, 100000);
|
|
19
|
+
localStorage.setItem(key, JSON.stringify(Object.fromEntries(Object.entries(drafts).slice(-20))));
|
|
20
|
+
}
|
|
21
|
+
return typeof drafts[projectId] === 'string' ? drafts[projectId] : '';
|
|
22
|
+
} catch { return ''; }
|
|
23
|
+
}
|
|
24
|
+
function rememberProjectDraft(projectId, draft) {
|
|
25
|
+
if (!projectId) return;
|
|
26
|
+
projectDrafts.set(projectId, draft);
|
|
27
|
+
savedProjectDraft(projectId, draft.text || '');
|
|
28
|
+
}
|
|
29
|
+
function savedProjectSession(projectId, sessionId) {
|
|
30
|
+
try {
|
|
31
|
+
const key = `kb.workbench.sessions:${apiBase}`;
|
|
32
|
+
const saved = JSON.parse(localStorage.getItem(key) || '{}');
|
|
33
|
+
if (sessionId !== undefined) {
|
|
34
|
+
if (sessionId) saved[projectId] = sessionId; else delete saved[projectId];
|
|
35
|
+
localStorage.setItem(key, JSON.stringify(saved));
|
|
36
|
+
}
|
|
37
|
+
return typeof saved[projectId] === 'string' ? saved[projectId] : null;
|
|
38
|
+
} catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
function forgetSessionSelection(sessionId) {
|
|
41
|
+
for (const project of state.projects) {
|
|
42
|
+
if (savedProjectSession(project.id) === sessionId) savedProjectSession(project.id, null);
|
|
43
|
+
const draft = projectDrafts.get(project.id);
|
|
44
|
+
if (draft?.sessionId === sessionId) projectDrafts.set(project.id, { ...draft, sessionId: null });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
4
48
|
const client = window.createClaudeWorkbenchClient
|
|
5
|
-
? window.createClaudeWorkbenchClient({ apiBase
|
|
49
|
+
? window.createClaudeWorkbenchClient({ apiBase })
|
|
6
50
|
: null;
|
|
7
51
|
|
|
8
52
|
const PERMISSION_LABELS = { default: '默认防护', acceptEdits: '接受编辑', plan: '计划模式', bypassPermissions: '完全访问' };
|
|
@@ -76,7 +120,7 @@ function applyPreferences(values) {
|
|
|
76
120
|
|
|
77
121
|
async function loadPreferences() {
|
|
78
122
|
try {
|
|
79
|
-
const response = await fetch(
|
|
123
|
+
const response = await fetch(`${apiBase}/ui-preferences`);
|
|
80
124
|
if (!response.ok) return;
|
|
81
125
|
const body = await response.json();
|
|
82
126
|
savedPreferences = { ...(body && body.values || {}) };
|
|
@@ -91,7 +135,7 @@ function savePreferences(partial) {
|
|
|
91
135
|
clearTimeout(prefsSaveTimer);
|
|
92
136
|
prefsSaveTimer = setTimeout(() => {
|
|
93
137
|
if (typeof fetch !== 'function') return; // non-browser harness (unit tests)
|
|
94
|
-
fetch(
|
|
138
|
+
fetch(`${apiBase}/ui-preferences`, {
|
|
95
139
|
method: 'PUT',
|
|
96
140
|
headers: { 'Content-Type': 'application/json' },
|
|
97
141
|
body: JSON.stringify(partial),
|
|
@@ -110,14 +154,13 @@ async function init() {
|
|
|
110
154
|
await Promise.all([refreshAgents(), refreshProfiles()]);
|
|
111
155
|
await loadPreferences();
|
|
112
156
|
await refreshProjects();
|
|
113
|
-
// 首个 kb:ready 在项目清单就绪前就已发出(上方 health/agents/profiles 都要
|
|
114
|
-
// 先跑完),壳届时下发的 kb:select-project 会扑空。清单就绪后再广播一次,
|
|
115
|
-
// 让壳重新选中项目,避免嵌入态卡在"未登记/无项目可选"。
|
|
116
|
-
if (window.parent !== window) window.parent.postMessage({ type: 'kb:ready', app: 'agent-terminal' }, '*');
|
|
117
157
|
wireChrome();
|
|
118
158
|
wireComposer();
|
|
119
159
|
wireSettings();
|
|
120
160
|
wireProjectModal();
|
|
161
|
+
// Ready is the usable-page boundary: the host may immediately select a
|
|
162
|
+
// project or open settings, so data and handlers must both be available.
|
|
163
|
+
if (window.parent !== window) window.parent.postMessage({ type: 'kb:ready', app: 'agent-terminal' }, '*');
|
|
121
164
|
}
|
|
122
165
|
|
|
123
166
|
/* ===================== agents & capabilities ===================== */
|
|
@@ -404,6 +447,7 @@ async function renderProjectListOnce() {
|
|
|
404
447
|
return;
|
|
405
448
|
}
|
|
406
449
|
for (const project of state.projects) {
|
|
450
|
+
if (embedded && project.id !== state.activeProjectId) continue;
|
|
407
451
|
const proj = el('div', 'proj' + (state.activeProjectId === project.id ? ' active' : ''));
|
|
408
452
|
proj.dataset.project = project.id;
|
|
409
453
|
const head = el('button', 'proj-item');
|
|
@@ -482,6 +526,7 @@ function sessionRow(summary, projectId) {
|
|
|
482
526
|
ev.stopPropagation();
|
|
483
527
|
try {
|
|
484
528
|
await client.archiveSession(summary.sessionId);
|
|
529
|
+
forgetSessionSelection(summary.sessionId);
|
|
485
530
|
if (state.activeSessionId === summary.sessionId) detachSession();
|
|
486
531
|
await renderProjectList();
|
|
487
532
|
} catch (error) { toast(error.message, true); }
|
|
@@ -499,6 +544,7 @@ async function renderArchived() {
|
|
|
499
544
|
list.textContent = '';
|
|
500
545
|
const rows = [];
|
|
501
546
|
for (const project of state.projects) {
|
|
547
|
+
if (embedded && project.id !== state.activeProjectId) continue;
|
|
502
548
|
let archived = [];
|
|
503
549
|
try { archived = await client.listSessions(project.id, { archived: true }); } catch { archived = []; }
|
|
504
550
|
for (const summary of archived) {
|
|
@@ -547,11 +593,13 @@ function shortWhen(iso) {
|
|
|
547
593
|
|
|
548
594
|
async function newSession() {
|
|
549
595
|
if (!state.activeProjectId) { openProjectModal(); return; }
|
|
596
|
+
const projectId = state.activeProjectId, epoch = selectionEpoch, creation = ++sessionCreationEpoch;
|
|
550
597
|
try {
|
|
551
598
|
const summary = await client.startSession(
|
|
552
599
|
{ contextId: state.activeProjectId, workspaceRef: state.activeProjectId, aiProfileId: state.currentProfileId, displayName: (state.projects.find(p => p.id === state.activeProjectId) || {}).name },
|
|
553
600
|
{ agentId: state.currentAgent, model: state.currentProfileId === undefined ? undefined : state.currentModel || undefined, permissionMode: state.permissionMode, effort: state.caps && state.caps.generationParams.includes('effort') ? state.effort : undefined },
|
|
554
601
|
);
|
|
602
|
+
if (epoch !== selectionEpoch || state.activeProjectId !== projectId || creation !== sessionCreationEpoch) { const draft = projectDrafts.get(projectId) || {}; if (!draft.sessionId) projectDrafts.set(projectId, { ...draft, sessionId: summary.sessionId }); return; }
|
|
555
603
|
if (state.activeSessionId === summary.sessionId) return;
|
|
556
604
|
state.sessionTitles.set(summary.sessionId, '新会话');
|
|
557
605
|
await openSession(summary.sessionId, { preserveAttachments: true });
|
|
@@ -561,14 +609,18 @@ async function newSession() {
|
|
|
561
609
|
}
|
|
562
610
|
|
|
563
611
|
async function openSession(sessionId, options = {}) {
|
|
612
|
+
sessionCreationEpoch++;
|
|
564
613
|
detachSession(options);
|
|
614
|
+
const epoch = sessionEpoch;
|
|
565
615
|
state.activeSessionId = sessionId;
|
|
566
616
|
resetStream();
|
|
567
617
|
let snapshot;
|
|
568
|
-
try { snapshot = await client.loadSession(sessionId); } catch (error) { state.activeSessionId = null; toast(error.message, true); return; }
|
|
618
|
+
try { snapshot = await client.loadSession(sessionId); } catch (error) { if (epoch !== sessionEpoch) return; state.activeSessionId = null; if (error.status === 404) forgetSessionSelection(sessionId); toast(error.message, true); return; }
|
|
619
|
+
if (epoch !== sessionEpoch || state.activeSessionId !== sessionId) return;
|
|
569
620
|
const summary = snapshot.session;
|
|
570
621
|
state.sessionSummary = summary;
|
|
571
622
|
state.activeProjectId = summary.contextId || state.activeProjectId;
|
|
623
|
+
if (state.activeProjectId) savedProjectSession(state.activeProjectId, sessionId);
|
|
572
624
|
state.currentAgent = summary.agentId || 'claude-code';
|
|
573
625
|
state.currentProfileId = summary.aiProfileId || null;
|
|
574
626
|
state.currentModel = summary.selectedModel || summary.model || null;
|
|
@@ -586,12 +638,24 @@ async function openSession(sessionId, options = {}) {
|
|
|
586
638
|
for (const event of snapshot.events || []) { renderEvent(event); last = Math.max(last, event.sequence || 0); }
|
|
587
639
|
} finally { state.replaying = false; }
|
|
588
640
|
rememberTitleFromEvents(snapshot.events || []);
|
|
589
|
-
state.unsub = client.subscribe(sessionId, event => renderEvent(event), () => {}, { afterSequence: last });
|
|
641
|
+
state.unsub = client.subscribe(sessionId, event => { if (epoch === sessionEpoch && state.activeSessionId === sessionId) renderEvent(event); }, () => {}, { afterSequence: last });
|
|
590
642
|
await renderProjectList();
|
|
591
643
|
refreshContextUsage();
|
|
644
|
+
if (embedded) {
|
|
645
|
+
try {
|
|
646
|
+
const response = await fetch(`${apiBase}/sessions/${encodeURIComponent(sessionId)}/knowledge`);
|
|
647
|
+
const knowledge = response.ok ? await response.json() : null;
|
|
648
|
+
if (epoch === sessionEpoch && state.activeSessionId === sessionId && knowledge) publishKnowledge(summary.contextId, sessionId, knowledge);
|
|
649
|
+
} catch { /* The native conversation remains usable if knowledge is offline. */ }
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function publishKnowledge(projectId, sessionId, knowledge) {
|
|
654
|
+
if (embedded) window.parent.postMessage({ type: 'kb:knowledge', app: 'agent-terminal', projectId, sessionId, knowledge }, document.referrer ? new URL(document.referrer).origin : location.origin);
|
|
592
655
|
}
|
|
593
656
|
|
|
594
657
|
function detachSession(options = {}) {
|
|
658
|
+
sessionEpoch++;
|
|
595
659
|
if (state.unsub) { try { state.unsub(); } catch {} state.unsub = null; }
|
|
596
660
|
state.activeSessionId = null;
|
|
597
661
|
state.sessionSummary = null;
|
|
@@ -1014,7 +1078,7 @@ function renderEvent(event) {
|
|
|
1014
1078
|
setMarkdown(body, String(event.result));
|
|
1015
1079
|
}
|
|
1016
1080
|
const lastMsg = [...col.children].reverse().find(node => node.classList && node.classList.contains('ai'));
|
|
1017
|
-
if (lastMsg) lastMsg.append(msgFooter(String(event.result || lastMsg.textContent), lastPrompt, { model: turnModel }));
|
|
1081
|
+
if (lastMsg) lastMsg.append(msgFooter(String(event.result || lastMsg.textContent), lastPrompt, { model: turnModel, at: event.at }));
|
|
1018
1082
|
if (turnChanges && turnChanges.size) col.append(renderTurnDiffs(turnChanges));
|
|
1019
1083
|
}
|
|
1020
1084
|
setBusyState('idle');
|
|
@@ -1218,7 +1282,8 @@ function msgFooter(text, lastPrompt, options = {}) {
|
|
|
1218
1282
|
const bits = [];
|
|
1219
1283
|
if (options.model) bits.push(options.model);
|
|
1220
1284
|
if (options.interrupted) bits.push('已中断');
|
|
1221
|
-
|
|
1285
|
+
const timestamp = options.at ? new Date(options.at) : state.replaying ? null : new Date();
|
|
1286
|
+
if (timestamp && !Number.isNaN(timestamp.getTime())) bits.push(timestamp.toTimeString().slice(0, 5));
|
|
1222
1287
|
row.append(el('span', 'foot-time', bits.join(' · ')));
|
|
1223
1288
|
return row;
|
|
1224
1289
|
}
|
|
@@ -1522,9 +1587,10 @@ function setBusyState(sessionState) {
|
|
|
1522
1587
|
|
|
1523
1588
|
async function refreshContextUsage() {
|
|
1524
1589
|
if (!state.activeSessionId) return;
|
|
1590
|
+
const sessionId = state.activeSessionId, epoch = sessionEpoch;
|
|
1525
1591
|
try {
|
|
1526
|
-
const usage = await fetch(
|
|
1527
|
-
if (!usage) return;
|
|
1592
|
+
const usage = await fetch(`${apiBase}/sessions/${encodeURIComponent(sessionId)}/context-usage`).then(r => (r.ok ? r.json() : null));
|
|
1593
|
+
if (!usage || epoch !== sessionEpoch || state.activeSessionId !== sessionId) return;
|
|
1528
1594
|
$('btn-ctx').hidden = false;
|
|
1529
1595
|
$('btn-ctx').title = usage.percent != null ? `上下文用量 ${usage.percent}%` : '上下文用量';
|
|
1530
1596
|
$('ctx-label').textContent = `${fmtTokens(usage.used)} / ${fmtTokens(usage.contextWindow)}`;
|
|
@@ -1565,6 +1631,7 @@ function wireComposer() {
|
|
|
1565
1631
|
input.addEventListener('input', () => {
|
|
1566
1632
|
input.style.height = 'auto';
|
|
1567
1633
|
input.style.height = `${Math.min(150, input.scrollHeight)}px`;
|
|
1634
|
+
if (embedded && state.activeProjectId) rememberProjectDraft(state.activeProjectId, { text: input.value, attachments: [...state.attachments], sessionId: state.activeSessionId });
|
|
1568
1635
|
});
|
|
1569
1636
|
input.addEventListener('keydown', event => {
|
|
1570
1637
|
if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendCurrent(); }
|
|
@@ -1655,26 +1722,37 @@ async function sendCurrent() {
|
|
|
1655
1722
|
const text = input.value.trim();
|
|
1656
1723
|
if (!text && !state.attachments.length) return;
|
|
1657
1724
|
state.sending = true;
|
|
1658
|
-
|
|
1725
|
+
const originProjectId = state.activeProjectId, originEpoch = selectionEpoch;
|
|
1726
|
+
if (!state.activeSessionId) { await newSession(); if (!state.activeSessionId || state.activeProjectId !== originProjectId || selectionEpoch !== originEpoch) { state.sending = false; return; } }
|
|
1659
1727
|
input.value = '';
|
|
1660
1728
|
input.style.height = 'auto';
|
|
1661
1729
|
const attachments = state.attachments.splice(0);
|
|
1662
1730
|
renderAttachmentTray();
|
|
1731
|
+
const sentSessionId = state.activeSessionId, sentProjectId = state.activeProjectId;
|
|
1732
|
+
rememberProjectDraft(sentProjectId, { text: '', attachments: [], sessionId: sentSessionId });
|
|
1663
1733
|
try {
|
|
1664
|
-
await client.send({
|
|
1734
|
+
const accepted = await client.send({
|
|
1665
1735
|
sessionId: state.activeSessionId,
|
|
1666
1736
|
text,
|
|
1667
1737
|
attachments: attachments.map(item => ({ name: item.name, mediaType: item.mediaType, data: item.data })),
|
|
1668
1738
|
// Effort rides on every message so mid-session switches take effect.
|
|
1669
1739
|
options: { effort: state.caps && state.caps.generationParams.includes('effort') ? state.effort : undefined },
|
|
1670
1740
|
});
|
|
1671
|
-
|
|
1741
|
+
if (accepted?.knowledge && state.activeSessionId === sentSessionId) publishKnowledge(sentProjectId, sentSessionId, accepted.knowledge);
|
|
1742
|
+
if (state.activeSessionId !== sentSessionId) return;
|
|
1743
|
+
setBusyState(accepted?.state || 'running');
|
|
1672
1744
|
state.follow = true; // sending a message re-engages follow mode
|
|
1673
1745
|
} catch (error) {
|
|
1746
|
+
if (state.activeProjectId !== sentProjectId || state.activeSessionId !== sentSessionId) {
|
|
1747
|
+
const draft = projectDrafts.get(sentProjectId) || {};
|
|
1748
|
+
rememberProjectDraft(sentProjectId, { ...draft, text: draft.text || text, attachments: [...attachments, ...(draft.attachments || [])], sessionId: sentSessionId });
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1674
1751
|
toast(error.message, true);
|
|
1675
1752
|
input.value = text;
|
|
1676
1753
|
state.attachments.unshift(...attachments);
|
|
1677
1754
|
renderAttachmentTray();
|
|
1755
|
+
rememberProjectDraft(sentProjectId, { text, attachments: [...state.attachments], sessionId: sentSessionId });
|
|
1678
1756
|
} finally {
|
|
1679
1757
|
state.sending = false;
|
|
1680
1758
|
}
|
|
@@ -1770,7 +1848,7 @@ function wireSettings() {
|
|
|
1770
1848
|
|
|
1771
1849
|
/* ===================== 会话捕获(终端对话是否留档) ===================== */
|
|
1772
1850
|
|
|
1773
|
-
const CAPTURE_API =
|
|
1851
|
+
const CAPTURE_API = `${apiBase}/capture-settings`;
|
|
1774
1852
|
|
|
1775
1853
|
function renderCaptureNote(text, bad) {
|
|
1776
1854
|
const note = $('capture-note');
|
|
@@ -2145,7 +2223,7 @@ function wireProjectModal() {
|
|
|
2145
2223
|
};
|
|
2146
2224
|
|
|
2147
2225
|
let pickingProject = false;
|
|
2148
|
-
const pickerUrl = () =>
|
|
2226
|
+
const pickerUrl = () => `${apiBase}/system/pick-folder`;
|
|
2149
2227
|
const addProjectViaPicker = async () => {
|
|
2150
2228
|
if (pickingProject) return;
|
|
2151
2229
|
pickingProject = true;
|
|
@@ -2278,10 +2356,8 @@ function wireChrome() {
|
|
|
2278
2356
|
* kb:sessions / kb:session / kb:archived / kb:removed-project(各操作回执)。
|
|
2279
2357
|
* URL 参数:?embed=1 进入嵌入态(默认折叠项目栏,sidebar=1 保持展开),project= 预选。 */
|
|
2280
2358
|
function embedAllowedOrigin(origin) {
|
|
2281
|
-
if (origin === 'null') return true; // file:// 壳
|
|
2282
2359
|
try {
|
|
2283
|
-
|
|
2284
|
-
return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]';
|
|
2360
|
+
return origin === (document.referrer ? new URL(document.referrer).origin : location.origin);
|
|
2285
2361
|
} catch { return false; }
|
|
2286
2362
|
}
|
|
2287
2363
|
function setEmbedSidebarVisible(visible) {
|
|
@@ -2296,6 +2372,7 @@ function embedSelectProject(ref) {
|
|
|
2296
2372
|
const project = embedFindProject(ref);
|
|
2297
2373
|
if (!project) return false;
|
|
2298
2374
|
if (state.activeProjectId !== project.id) {
|
|
2375
|
+
if (state.activeProjectId) rememberProjectDraft(state.activeProjectId, { text: $('chat-input').value, attachments: [...state.attachments], sessionId: state.activeSessionId });
|
|
2299
2376
|
detachSession();
|
|
2300
2377
|
resetStream();
|
|
2301
2378
|
setBusyState('idle');
|
|
@@ -2306,18 +2383,33 @@ function embedSelectProject(ref) {
|
|
|
2306
2383
|
$('session-title').textContent = '新会话';
|
|
2307
2384
|
applyCapabilityGates();
|
|
2308
2385
|
renderModelLabel();
|
|
2386
|
+
const draft = projectDrafts.get(project.id) || { text: savedProjectDraft(project.id), sessionId: savedProjectSession(project.id) };
|
|
2387
|
+
$('chat-input').value = draft?.text || '';
|
|
2388
|
+
state.attachments = draft?.attachments || [];
|
|
2389
|
+
renderAttachmentTray();
|
|
2390
|
+
if (draft?.sessionId) openSession(draft.sessionId, { preserveAttachments: true });
|
|
2391
|
+
else publishKnowledge(project.id, null, { status: 'not-retrieved', references: [] });
|
|
2392
|
+
}
|
|
2393
|
+
if (!state.activeSessionId) {
|
|
2394
|
+
const sessionId = projectDrafts.get(project.id)?.sessionId || savedProjectSession(project.id);
|
|
2395
|
+
if (sessionId) openSession(sessionId, { preserveAttachments: true });
|
|
2309
2396
|
}
|
|
2310
2397
|
state.expanded.add(project.id);
|
|
2311
2398
|
renderProjectList();
|
|
2312
2399
|
return true;
|
|
2313
2400
|
}
|
|
2314
2401
|
window.addEventListener('message', async ev => {
|
|
2315
|
-
if (!embedAllowedOrigin(ev.origin)) return;
|
|
2402
|
+
if (ev.source !== window.parent || !embedAllowedOrigin(ev.origin)) return;
|
|
2316
2403
|
const data = ev.data;
|
|
2317
2404
|
if (!data || typeof data !== 'object') return;
|
|
2318
|
-
const reply = payload => { if (ev.source) ev.source.postMessage(Object.assign({ app: 'agent-terminal' }, payload),
|
|
2405
|
+
const reply = payload => { if (ev.source) ev.source.postMessage(Object.assign({ app: 'agent-terminal', requestId: data.requestId }, payload), ev.origin); };
|
|
2406
|
+
if (data.type === 'kb:theme') { document.body.classList.toggle('light', data.theme !== 'dark'); document.body.dataset.kbTheme = data.theme === 'dark' ? 'dark' : 'light'; reply({ type: 'kb:configured', theme: document.body.dataset.kbTheme }); return; }
|
|
2407
|
+
if (data.type === 'kb:open-settings') { openSettings('providers'); return; }
|
|
2319
2408
|
if (data.type === 'kb:sidebar') setEmbedSidebarVisible(!!data.visible);
|
|
2320
2409
|
else if (data.type === 'kb:select-project') {
|
|
2410
|
+
const epoch = ++selectionEpoch;
|
|
2411
|
+
if (!embedFindProject(data.project)) await refreshProjects();
|
|
2412
|
+
if (epoch !== selectionEpoch) return;
|
|
2321
2413
|
reply({ type: 'kb:projects', ok: embedSelectProject(data.project), activeProjectId: state.activeProjectId,
|
|
2322
2414
|
projects: state.projects.map(p => ({ id: p.id, name: p.name, rootPath: p.rootPath })) });
|
|
2323
2415
|
} else if (data.type === 'kb:list-sessions') {
|
|
@@ -2345,6 +2437,7 @@ window.addEventListener('message', async ev => {
|
|
|
2345
2437
|
const sessionId = String(data.sessionId || '');
|
|
2346
2438
|
try {
|
|
2347
2439
|
await client.archiveSession(sessionId);
|
|
2440
|
+
forgetSessionSelection(sessionId);
|
|
2348
2441
|
if (state.activeSessionId === sessionId) detachSession();
|
|
2349
2442
|
await renderProjectList();
|
|
2350
2443
|
reply({ type: 'kb:archived', ok: true, sessionId });
|
|
@@ -2370,14 +2463,16 @@ window.addEventListener('message', async ev => {
|
|
|
2370
2463
|
function embedInit() {
|
|
2371
2464
|
const q = new URLSearchParams(location.search);
|
|
2372
2465
|
if (q.get('embed') === '1') {
|
|
2373
|
-
|
|
2466
|
+
document.body.classList.add('kb-embedded');
|
|
2467
|
+
document.body.classList.toggle('light', q.get('theme') !== 'dark');
|
|
2468
|
+
document.body.dataset.kbTheme = q.get('theme') === 'dark' ? 'dark' : 'light';
|
|
2469
|
+
setEmbedSidebarVisible(true);
|
|
2374
2470
|
const preset = q.get('project');
|
|
2375
2471
|
if (preset) {
|
|
2376
2472
|
const trySelect = () => { if (!embedSelectProject(preset)) setTimeout(trySelect, 500); };
|
|
2377
2473
|
setTimeout(trySelect, 0);
|
|
2378
2474
|
}
|
|
2379
2475
|
}
|
|
2380
|
-
if (window.parent !== window) window.parent.postMessage({ type: 'kb:ready', app: 'agent-terminal' }, '*');
|
|
2381
2476
|
}
|
|
2382
2477
|
embedInit();
|
|
2383
2478
|
// 主区顶栏常驻设置入口(嵌入折叠侧栏后仍可达;与模型弹层内「管理模型」同目标)
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
<div class="col" id="stream-col">
|
|
45
45
|
<div class="empty-hint" id="empty-hint">
|
|
46
46
|
<div class="big"><span class="hint-wrap"><span class="hint-art"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 8 4 4-4 4"/><path d="M13 16h6"/></svg></span><span class="hint-spark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3l1.9 5.8L20 12l-6.1 3.2L12 21l-1.9-5.8L4 12l6.1-3.2z"/></svg></span></span></div>
|
|
47
|
-
<p>从左侧选择一个项目,或 <button class="linklike" id="hint-add-proj">添加项目</button> 开始。<br>
|
|
47
|
+
<p class="standalone-hint">从左侧选择一个项目,或 <button class="linklike" id="hint-add-proj">添加项目</button> 开始。<br>
|
|
48
48
|
新会话将使用底部选中的 Agent 与模型。</p>
|
|
49
|
+
<p class="embedded-hint">输入当前项目的任务,开始新会话。<br>相关知识会在发送前检索,并在右侧显示来源。</p>
|
|
49
50
|
</div>
|
|
50
51
|
</div>
|
|
51
52
|
</div>
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
<button class="bar-btn" id="btn-ctx" title="上下文用量" hidden><span id="ctx-label"></span></button>
|
|
68
69
|
<div class="pop from-right ctx-pop" id="pop-ctx" hidden></div>
|
|
69
70
|
</span>
|
|
70
|
-
<span class="anchor">
|
|
71
|
+
<span class="anchor model-anchor">
|
|
71
72
|
<button class="bar-btn" id="btn-model" title="选择供应商与模型"><span id="model-label">Agent 自身登录 · 默认模型</span> <span class="chev"><svg class="ics" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span></button>
|
|
72
73
|
<div class="pop from-right" id="pop-model" hidden></div>
|
|
73
74
|
</span>
|
|
@@ -152,7 +153,7 @@
|
|
|
152
153
|
|
|
153
154
|
<div class="toast-host" id="toast" aria-live="polite"></div>
|
|
154
155
|
|
|
155
|
-
<script src="
|
|
156
|
+
<script src="../packages/client/index.js"></script>
|
|
156
157
|
<script src="./app.js"></script>
|
|
157
158
|
</body>
|
|
158
159
|
</html>
|
package/docs/sdk/openapi.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"browser": "index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@claude-ai-workbench/contracts": "0.
|
|
23
|
+
"@claude-ai-workbench/contracts": "0.13.1"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"browser": "index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@claude-ai-workbench/contracts": "0.
|
|
23
|
+
"@claude-ai-workbench/contracts": "0.13.1"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
|
@@ -25,12 +25,17 @@ app.server.listen(port, host, () => {
|
|
|
25
25
|
// Compile the folder-picker helper up front so the first "browse…" click
|
|
26
26
|
// pops the dialog in milliseconds instead of paying the powershell +
|
|
27
27
|
// Add-Type startup. Never blocks or crashes the server.
|
|
28
|
-
warmFolderPicker({ title: '选择项目文件夹' }).catch(() => {});
|
|
28
|
+
if (process.env.KB_WORKBENCH_DISABLE_PICKER !== '1') warmFolderPicker({ title: '选择项目文件夹' }).catch(() => {});
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
let stopping = false;
|
|
31
32
|
function shutdown() {
|
|
33
|
+
if (stopping) return; stopping = true;
|
|
34
|
+
app.service.destroy();
|
|
35
|
+
app.server.closeAllConnections?.();
|
|
32
36
|
app.server.close(() => process.exit(0));
|
|
33
37
|
setTimeout(() => process.exit(0), 3000).unref();
|
|
34
38
|
}
|
|
35
39
|
process.on('SIGINT', shutdown);
|
|
36
40
|
process.on('SIGTERM', shutdown);
|
|
41
|
+
process.on('message', message => { if (message?.type === 'kb:shutdown') shutdown(); });
|
package/packages/server/index.js
CHANGED
|
@@ -15,7 +15,8 @@ const { AttachmentStore } = require('./lib/attachment-store');
|
|
|
15
15
|
const { ProjectStore } = require('./lib/project-store');
|
|
16
16
|
const { SessionArchiveStore } = require('./lib/session-archive-store');
|
|
17
17
|
const { AgentRegistry } = require('./lib/agent-registry');
|
|
18
|
-
const { createAgentDiscovery,
|
|
18
|
+
const { createAgentDiscovery, probeExecutable } = require('./lib/agent-discovery');
|
|
19
|
+
const { createDiagnostics } = require('./lib/diagnostics');
|
|
19
20
|
const { createSystemDoctor } = require('./lib/system-doctor');
|
|
20
21
|
const { createModelDetection } = require('./lib/model-detection');
|
|
21
22
|
const { createCodexDriver } = require('./lib/drivers/codex-driver');
|
|
@@ -38,7 +39,10 @@ function createWorkbenchServer(options = {}) {
|
|
|
38
39
|
...projects.toWorkspaceMap(),
|
|
39
40
|
};
|
|
40
41
|
const profiles = options.profiles || new ProfileStore({ dataDir });
|
|
41
|
-
const
|
|
42
|
+
const bridgeUrl = options.bridgeUrl || process.env.KB_EVENT_BRIDGE_URL;
|
|
43
|
+
const runtimeToken = options.runtimeToken || process.env.KB_WORKBENCH_RUNTIME_TOKEN;
|
|
44
|
+
const conversationCapture = options.conversationCapture || (bridgeUrl && runtimeToken ? new (require('./lib/conversation-capture').ConversationCapture)({ file: path.join(dataDir, 'capture-outbox.v1.json'), bridgeUrl, token: runtimeToken }) : null);
|
|
45
|
+
const captureSettings = options.captureSettings || new CaptureSettingsStore({ dataDir, defaultConversations: !!conversationCapture });
|
|
42
46
|
const uiPreferences = options.uiPreferences || new UiPreferencesStore({ dataDir });
|
|
43
47
|
const attachments = options.attachments || new AttachmentStore({ root: path.join(dataDir, 'attachments') });
|
|
44
48
|
const contexts = options.contexts || new ContextResolver({ workspaces, createMissing: options.createMissingWorkspaces !== false });
|
|
@@ -48,7 +52,9 @@ function createWorkbenchServer(options = {}) {
|
|
|
48
52
|
// the driver at boot and re-applied on every settings save.
|
|
49
53
|
const applyBridgeCapture = enabled => {
|
|
50
54
|
if (claudeDriver && typeof claudeDriver.setBridgeCaptureConversation === 'function') {
|
|
51
|
-
|
|
55
|
+
// Native capture records the original input, not the augmented SDK
|
|
56
|
+
// prompt. Suppress inherited CLI hooks to avoid duplicate capture.
|
|
57
|
+
claudeDriver.setBridgeCaptureConversation(conversationCapture ? false : enabled);
|
|
52
58
|
}
|
|
53
59
|
};
|
|
54
60
|
applyBridgeCapture(captureSettings.read().terminalConversations);
|
|
@@ -63,6 +69,7 @@ function createWorkbenchServer(options = {}) {
|
|
|
63
69
|
},
|
|
64
70
|
});
|
|
65
71
|
const runner = registry.asRunner();
|
|
72
|
+
const diagnostics = options.logger || createDiagnostics({ dataDir, secrets: () => [...Object.values(profiles._credentials().credentials), process.env.ANTHROPIC_API_KEY, process.env.ANTHROPIC_AUTH_TOKEN] });
|
|
66
73
|
const agentDiscovery = options.agentDiscovery || createAgentDiscovery({
|
|
67
74
|
claudeProbe: () => {
|
|
68
75
|
try {
|
|
@@ -72,11 +79,17 @@ function createWorkbenchServer(options = {}) {
|
|
|
72
79
|
const cmd = typeof executable === 'string'
|
|
73
80
|
? executable
|
|
74
81
|
: executable && executable.cmd;
|
|
75
|
-
if (!cmd)
|
|
82
|
+
if (!cmd) {
|
|
83
|
+
const reason = executable?.reason || '未发现可用的 Claude Code 启动文件。';
|
|
84
|
+
diagnostics.error('agent.discovery.failed', 'Claude Code 不可用。', { phase: 'discovery', error: { code: 'CLAUDE_EXECUTABLE_NOT_FOUND', message: reason }, context: { agent: 'claude-code', rejectedPaths: executable?.rejected || [] } });
|
|
85
|
+
return { available: false, version: null, reason };
|
|
86
|
+
}
|
|
76
87
|
// Honest probe: spawn the resolved executable with --version. Same
|
|
77
88
|
// contract as every other agent: no version, no availability claim.
|
|
78
|
-
const
|
|
79
|
-
|
|
89
|
+
const probe = probeExecutable(cmd);
|
|
90
|
+
if (!probe.available) diagnostics.error('agent.discovery.failed', 'Claude Code 版本探测失败。', { phase: 'version-probe', error: { code: probe.code, message: probe.reason }, context: { agent: 'claude-code', executable: cmd, exitCode: probe.exitCode } });
|
|
91
|
+
else if (executable.rejected?.length) diagnostics.warn('agent.discovery.recovered', '已跳过失效的 Claude Code 启动路径,使用有效安装。', { phase: 'discovery', context: { executable: cmd, rejectedPaths: executable.rejected } });
|
|
92
|
+
return probe;
|
|
80
93
|
} catch { return { available: false, version: null }; }
|
|
81
94
|
},
|
|
82
95
|
});
|
|
@@ -84,23 +97,31 @@ function createWorkbenchServer(options = {}) {
|
|
|
84
97
|
const modelDetection = options.modelDetection || createModelDetection({
|
|
85
98
|
contextLookup: modelContextWindows && modelContextWindows.lookupContextWindow,
|
|
86
99
|
});
|
|
87
|
-
const
|
|
100
|
+
const collectDoctor = options.doctor || createSystemDoctor({
|
|
88
101
|
resolveClaudeExecutable: () => {
|
|
89
102
|
try { return claudeDriver.findClaudeExecutableForSdk(); } catch { return { cmd: null }; }
|
|
90
103
|
},
|
|
91
104
|
discoverZCodeCli,
|
|
92
105
|
probeZCodeVersion,
|
|
93
106
|
});
|
|
107
|
+
const doctor = () => {
|
|
108
|
+
const report = collectDoctor();
|
|
109
|
+
const claude = report.agents?.['claude-code'];
|
|
110
|
+
if (claude && !claude.versionProbe?.ok) diagnostics.error('agent.doctor.failed', 'Claude Code 环境诊断失败。', { phase: 'doctor', error: { code: 'CLAUDE_PROBE_FAILED', message: claude.versionProbe?.error || claude.resolutionReason }, context: { agent: 'claude-code', executable: claude.resolvedExecutable, rejectedPaths: claude.rejectedPaths, exitCode: claude.versionProbe?.exitCode } });
|
|
111
|
+
return report;
|
|
112
|
+
};
|
|
94
113
|
const service = options.service || new WorkbenchService({
|
|
95
|
-
|
|
114
|
+
knowledge: options.knowledge || new (require('./lib/knowledge-context').KnowledgeContext)({ engineUrl: options.engineUrl || process.env.KB_KNOWLEDGE_ENGINE_URL, contextFile: path.join(dataDir, 'knowledge-contexts.v1.json') }),
|
|
115
|
+
runner, profiles, contexts, attachments, apiPrefix, diagnostics,
|
|
96
116
|
projects, sessionArchive, agentDiscovery, modelContextWindows, modelDetection,
|
|
97
117
|
archiveRetentionDays: options.archiveRetentionDays,
|
|
98
118
|
captureSettings, uiPreferences, onCaptureSettingsSaved: applyBridgeCapture,
|
|
119
|
+
conversationCapture,
|
|
99
120
|
testWorkspace: path.join(testRoot, 'profile-test'), auditFile: path.join(dataDir, 'audit.jsonl'),
|
|
100
121
|
version: options.version || require('./package.json').version,
|
|
101
122
|
maxSessions: options.maxSessions, maxConcurrentRuns: options.maxConcurrentRuns,
|
|
102
123
|
});
|
|
103
|
-
const handleApi = createHttpHandler(service, { apiPrefix, corsOrigin: options.corsOrigin, doctor });
|
|
124
|
+
const handleApi = createHttpHandler(service, { apiPrefix, corsOrigin: options.corsOrigin, doctor, runtimeToken: options.runtimeToken || process.env.KB_WORKBENCH_RUNTIME_TOKEN });
|
|
104
125
|
const distributionRoot = resolveDistributionRoot();
|
|
105
126
|
const staticRoot = path.resolve(options.staticRoot || path.join(distributionRoot, 'apps', 'minimal-test-host'));
|
|
106
127
|
const profileSettingsRoot = path.resolve(options.profileSettingsRoot || path.join(distributionRoot, 'apps', 'ai-profile-settings-host'));
|