@sanqianx/claude-ai-workbench 0.13.2 → 0.13.4
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/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +652 -613
- package/apps/agent-terminal/app.js +143 -67
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +172 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +189 -159
- package/apps/agent-terminal/right-workspace-dock.js +652 -521
- package/apps/agent-terminal/user-question-card.js +68 -0
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +15 -13
- package/docs/sdk/openapi.yaml +16 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1461 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/user-questions.js +67 -0
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2408 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- package/src/sdk-wave-e.ts +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
(function (global) {
|
|
2
|
+
'use strict';
|
|
3
|
+
let serial = 0;
|
|
4
|
+
function createUserQuestionCard({ document: doc, event, storageKey, onSubmit }) {
|
|
5
|
+
const make = (tag, cls, text) => { const node = doc.createElement(tag); if (cls) node.className = cls; if (text) node.textContent = text; return node; };
|
|
6
|
+
const node = make('section', 'perm question-card'); node.dataset.permId = event.requestId;
|
|
7
|
+
node.setAttribute('aria-label', '等待你的选择');
|
|
8
|
+
const title = make('div', 'question-title', '等待你的选择');
|
|
9
|
+
const form = make('form', 'question-form');
|
|
10
|
+
const fields = make('div', 'question-fields');
|
|
11
|
+
const error = make('div', 'question-error'); error.setAttribute('role', 'alert');
|
|
12
|
+
const submit = make('button', 'btn-allow question-submit', '提交答案'); submit.type = 'submit'; submit.disabled = true;
|
|
13
|
+
const footer = make('div', 'question-footer'); footer.append(make('span', '', '提交后继续'), submit);
|
|
14
|
+
form.append(fields, error, footer); node.append(title, form);
|
|
15
|
+
const readers = [], questions = event.questions || [], group = `question-${++serial}`;
|
|
16
|
+
let saved = {}, sending = false, settled = false;
|
|
17
|
+
try { saved = JSON.parse(global.localStorage?.getItem(storageKey) || '{}'); } catch {}
|
|
18
|
+
const read = () => Object.fromEntries(readers.map(reader => [reader.id, reader.read()]));
|
|
19
|
+
const update = () => {
|
|
20
|
+
const answers = read();
|
|
21
|
+
submit.disabled = sending || settled || readers.some(reader => !answers[reader.id].length || reader.incomplete());
|
|
22
|
+
const draft = Object.fromEntries(questions.filter(question => !question.isSecret).map(question => [question.id, answers[question.id]]));
|
|
23
|
+
try { if (storageKey) global.localStorage?.setItem(storageKey, JSON.stringify(draft)); } catch {}
|
|
24
|
+
};
|
|
25
|
+
questions.forEach((question, index) => {
|
|
26
|
+
const field = make('fieldset', 'question-field');
|
|
27
|
+
field.append(make('legend', '', question.question));
|
|
28
|
+
const options = [], custom = question.allowCustom ? make('input', 'question-custom') : null;
|
|
29
|
+
const previous = question.isSecret ? [] : Array.isArray(saved[question.id]) ? saved[question.id] : [];
|
|
30
|
+
let other = null;
|
|
31
|
+
const addOption = (text, description, value, checked) => {
|
|
32
|
+
const label = make('label', 'question-option');
|
|
33
|
+
const input = make('input'); input.type = question.multiSelect ? 'checkbox' : 'radio'; input.name = `${group}-${index}`; input.value = value; input.checked = checked;
|
|
34
|
+
const copy = make('span', 'question-option-copy'); copy.append(make('span', 'question-option-label', text));
|
|
35
|
+
if (description) copy.append(make('span', 'question-option-description', description));
|
|
36
|
+
label.append(input, copy); field.append(label); input.addEventListener('change', update); return input;
|
|
37
|
+
};
|
|
38
|
+
for (const option of question.options || []) options.push(addOption(option.label, option.description, option.label, previous.includes(option.label)));
|
|
39
|
+
if (custom) {
|
|
40
|
+
const own = previous.filter(value => !question.options.some(option => option.label === value));
|
|
41
|
+
if (question.options.length) other = addOption('其他', '', '__custom__', own.length > 0);
|
|
42
|
+
custom.type = question.isSecret ? 'password' : 'text'; custom.placeholder = question.options.length ? '输入你的选择或要求' : '输入答案';
|
|
43
|
+
custom.setAttribute('aria-label', question.question + ':自定义答案'); custom.value = own.join(', '); custom.autocomplete = 'off';
|
|
44
|
+
custom.addEventListener('input', () => { if (other) { if (!question.multiSelect) options.forEach(option => { option.checked = false; }); other.checked = true; } update(); });
|
|
45
|
+
field.append(custom);
|
|
46
|
+
}
|
|
47
|
+
readers.push({ id: question.id,
|
|
48
|
+
read: () => [...options.filter(option => option.checked).map(option => option.value), ...(custom && (!other || other.checked) && custom.value.trim() ? [custom.value.trim()] : [])],
|
|
49
|
+
incomplete: () => Boolean(custom && (!other || other.checked) && !custom.value.trim()),
|
|
50
|
+
}); fields.append(field);
|
|
51
|
+
});
|
|
52
|
+
node.settle = result => {
|
|
53
|
+
settled = true; form.hidden = true; title.textContent = result?.cancelled || result?.allow === false ? '问题已取消,未提交选择' : '答案已提交';
|
|
54
|
+
node.classList.add('question-settled');
|
|
55
|
+
try { if (storageKey) global.localStorage?.removeItem(storageKey); } catch {}
|
|
56
|
+
};
|
|
57
|
+
form.addEventListener('submit', async action => {
|
|
58
|
+
action.preventDefault(); update(); if (submit.disabled) return;
|
|
59
|
+
sending = true; error.textContent = ''; update();
|
|
60
|
+
try { await onSubmit(read()); node.settle({ allow: true }); }
|
|
61
|
+
catch { error.textContent = '答案未提交,请重试。'; }
|
|
62
|
+
finally { sending = false; if (!settled) update(); }
|
|
63
|
+
});
|
|
64
|
+
update(); return node;
|
|
65
|
+
}
|
|
66
|
+
global.createUserQuestionCard = createUserQuestionCard;
|
|
67
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = { createUserQuestionCard };
|
|
68
|
+
})(typeof window === 'undefined' ? globalThis : window);
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>AI Profile Settings</title>
|
|
7
|
-
<link rel="stylesheet" href="/packages/ui/theme.css">
|
|
8
|
-
<link rel="stylesheet" href="page.css">
|
|
9
|
-
<script src="/packages/client/index.js"></script>
|
|
10
|
-
<script src="/packages/ui/ai-profile-settings.js"></script>
|
|
11
|
-
</head>
|
|
12
|
-
<body>
|
|
13
|
-
<main class="preview">
|
|
14
|
-
<section class="profile-module">
|
|
15
|
-
<div class="module-bar" id="moduleBar">
|
|
16
|
-
<strong>AI 模型</strong>
|
|
17
|
-
<div class="bar-actions"><span id="listFeedback"></span><button id="addProfile">新增</button></div>
|
|
18
|
-
</div>
|
|
19
|
-
<ai-profile-list id="profileList"></ai-profile-list>
|
|
20
|
-
<ai-profile-editor id="profileEditor" class="is-hidden"></ai-profile-editor>
|
|
21
|
-
</section>
|
|
22
|
-
</main>
|
|
23
|
-
<script src="page.js"></script>
|
|
24
|
-
</body>
|
|
25
|
-
</html>
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>AI Profile Settings</title>
|
|
7
|
+
<link rel="stylesheet" href="/packages/ui/theme.css">
|
|
8
|
+
<link rel="stylesheet" href="page.css">
|
|
9
|
+
<script src="/packages/client/index.js"></script>
|
|
10
|
+
<script src="/packages/ui/ai-profile-settings.js"></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main class="preview">
|
|
14
|
+
<section class="profile-module">
|
|
15
|
+
<div class="module-bar" id="moduleBar">
|
|
16
|
+
<strong>AI 模型</strong>
|
|
17
|
+
<div class="bar-actions"><span id="listFeedback"></span><button id="addProfile">新增</button></div>
|
|
18
|
+
</div>
|
|
19
|
+
<ai-profile-list id="profileList"></ai-profile-list>
|
|
20
|
+
<ai-profile-editor id="profileEditor" class="is-hidden"></ai-profile-editor>
|
|
21
|
+
</section>
|
|
22
|
+
</main>
|
|
23
|
+
<script src="page.js"></script>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
*{box-sizing:border-box}html,body{min-height:100%;margin:0}body{padding:24px;background:#f5f6f7;color:var(--caw-text);font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}.preview{width:min(680px,100%);margin:0 auto;padding:16px;border:1px solid var(--caw-line);border-radius:10px;background:var(--caw-panel);box-shadow:0 8px 24px rgba(24,34,48,.06)}.profile-module{min-width:0}.module-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:9px}.module-bar strong{font-size:13px}.bar-actions{display:flex;align-items:center;gap:9px}.bar-actions span{max-width:390px;color:var(--caw-muted);font-size:9px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-actions span.good{color:var(--caw-good)}.bar-actions span.bad{color:var(--caw-bad)}.bar-actions button{height:32px;padding:0 11px;border:0;border-radius:7px;background:var(--caw-accent);color:#fff;font:11px inherit;cursor:pointer}.is-hidden{display:none}@media(max-width:520px){body{padding:10px}.preview{padding:12px}.bar-actions span{max-width:180px}}
|
|
1
|
+
*{box-sizing:border-box}html,body{min-height:100%;margin:0}body{padding:24px;background:#f5f6f7;color:var(--caw-text);font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}.preview{width:min(680px,100%);margin:0 auto;padding:16px;border:1px solid var(--caw-line);border-radius:10px;background:var(--caw-panel);box-shadow:0 8px 24px rgba(24,34,48,.06)}.profile-module{min-width:0}.module-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:9px}.module-bar strong{font-size:13px}.bar-actions{display:flex;align-items:center;gap:9px}.bar-actions span{max-width:390px;color:var(--caw-muted);font-size:9px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-actions span.good{color:var(--caw-good)}.bar-actions span.bad{color:var(--caw-bad)}.bar-actions button{height:32px;padding:0 11px;border:0;border-radius:7px;background:var(--caw-accent);color:#fff;font:11px inherit;cursor:pointer}.is-hidden{display:none}@media(max-width:520px){body{padding:10px}.preview{padding:12px}.bar-actions span{max-width:180px}}
|
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
(function () {
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const client = createClaudeWorkbenchClient({ apiBase: "/api/claude-workbench/v1" });
|
|
5
|
-
const moduleBar = document.querySelector("#moduleBar");
|
|
6
|
-
const listFeedback = document.querySelector("#listFeedback");
|
|
7
|
-
const addProfile = document.querySelector("#addProfile");
|
|
8
|
-
const profileList = document.querySelector("#profileList");
|
|
9
|
-
const profileEditor = document.querySelector("#profileEditor");
|
|
10
|
-
|
|
11
|
-
function setListStatus(message, tone = "") {
|
|
12
|
-
listFeedback.textContent = String(message || "");
|
|
13
|
-
listFeedback.className = tone;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function testErrorMessage(error) {
|
|
17
|
-
const message = String(error?.message || "连接测试失败");
|
|
18
|
-
if (/timed out|timeout/i.test(message)) return "连接测试超时,请检查请求地址、网络或模型名称";
|
|
19
|
-
if (/invalid.*(?:api )?key|unauthorized|\b401\b/i.test(message)) return "API Key 无效或未授权";
|
|
20
|
-
if (/model.*not found|\b404\b/i.test(message)) return "模型名称或请求地址不存在";
|
|
21
|
-
return message;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function showList(message = "", tone = "") {
|
|
25
|
-
moduleBar.classList.remove("is-hidden");
|
|
26
|
-
profileList.classList.remove("is-hidden");
|
|
27
|
-
profileEditor.classList.add("is-hidden");
|
|
28
|
-
profileList.busy = false;
|
|
29
|
-
setListStatus(message, tone);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function showEditor(profile) {
|
|
33
|
-
moduleBar.classList.add("is-hidden");
|
|
34
|
-
profileList.classList.add("is-hidden");
|
|
35
|
-
profileEditor.classList.remove("is-hidden");
|
|
36
|
-
profileEditor.profile = profile || null;
|
|
37
|
-
profileEditor.setBusy(false);
|
|
38
|
-
profileEditor.setStatus("");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function refresh() {
|
|
42
|
-
profileList.profiles = await client.listProfiles();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function persist(profile, credential) {
|
|
46
|
-
const saved = await client.saveProfile(profile);
|
|
47
|
-
if (credential) await client.setCredential(saved.id, credential);
|
|
48
|
-
return saved;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
addProfile.addEventListener("click", () => showEditor(null));
|
|
52
|
-
profileList.addEventListener("profile-edit", (event) => showEditor(event.detail.profile));
|
|
53
|
-
profileEditor.addEventListener("profile-cancel", () => showList());
|
|
54
|
-
|
|
55
|
-
profileEditor.addEventListener("profile-save", async (event) => {
|
|
56
|
-
profileEditor.setBusy(true, "保存中…");
|
|
57
|
-
try {
|
|
58
|
-
await persist(event.detail.profile, event.detail.credential);
|
|
59
|
-
await refresh();
|
|
60
|
-
profileEditor.setBusy(false);
|
|
61
|
-
showList("已保存,配置现在可以使用", "good");
|
|
62
|
-
} catch (error) {
|
|
63
|
-
profileEditor.setBusy(false);
|
|
64
|
-
profileEditor.setStatus(error.message, "bad");
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
profileEditor.addEventListener("profile-test", async (event) => {
|
|
69
|
-
profileEditor.setBusy(true, "测试中…");
|
|
70
|
-
try {
|
|
71
|
-
const result = await client.testProfileDraft(event.detail.profile, event.detail.credential);
|
|
72
|
-
profileEditor.setBusy(false);
|
|
73
|
-
profileEditor.setStatus(
|
|
74
|
-
result.runtimeVerified ? `连接成功 · ${result.model || event.detail.profile.models.default} · ${result.durationMs}ms;请点击保存后生效` : "连接测试失败",
|
|
75
|
-
result.runtimeVerified ? "good" : "bad",
|
|
76
|
-
);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
profileEditor.setBusy(false);
|
|
79
|
-
profileEditor.setStatus(testErrorMessage(error), "bad");
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
profileList.addEventListener("profile-test", async (event) => {
|
|
84
|
-
profileList.busy = true;
|
|
85
|
-
setListStatus("测试中…");
|
|
86
|
-
try {
|
|
87
|
-
const result = await client.testProfileDraft(event.detail.profile, "");
|
|
88
|
-
profileList.busy = false;
|
|
89
|
-
setListStatus(result.runtimeVerified ? `连接成功 · ${result.durationMs}ms` : "连接测试失败", result.runtimeVerified ? "good" : "bad");
|
|
90
|
-
} catch (error) {
|
|
91
|
-
profileList.busy = false;
|
|
92
|
-
setListStatus(testErrorMessage(error), "bad");
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
profileList.addEventListener("profile-delete", async (event) => {
|
|
97
|
-
if (!window.confirm(`删除“${event.detail.profile.name}”?`)) return;
|
|
98
|
-
profileList.busy = true;
|
|
99
|
-
setListStatus("删除中…");
|
|
100
|
-
try {
|
|
101
|
-
await client.deleteProfile(event.detail.profile.id);
|
|
102
|
-
await refresh();
|
|
103
|
-
profileList.busy = false;
|
|
104
|
-
setListStatus("已删除", "good");
|
|
105
|
-
} catch (error) {
|
|
106
|
-
profileList.busy = false;
|
|
107
|
-
setListStatus(error.message, "bad");
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
window.addEventListener("beforeunload", () => client.destroy());
|
|
112
|
-
refresh().catch((error) => setListStatus(error.message, "bad"));
|
|
113
|
-
})();
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const client = createClaudeWorkbenchClient({ apiBase: "/api/claude-workbench/v1" });
|
|
5
|
+
const moduleBar = document.querySelector("#moduleBar");
|
|
6
|
+
const listFeedback = document.querySelector("#listFeedback");
|
|
7
|
+
const addProfile = document.querySelector("#addProfile");
|
|
8
|
+
const profileList = document.querySelector("#profileList");
|
|
9
|
+
const profileEditor = document.querySelector("#profileEditor");
|
|
10
|
+
|
|
11
|
+
function setListStatus(message, tone = "") {
|
|
12
|
+
listFeedback.textContent = String(message || "");
|
|
13
|
+
listFeedback.className = tone;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function testErrorMessage(error) {
|
|
17
|
+
const message = String(error?.message || "连接测试失败");
|
|
18
|
+
if (/timed out|timeout/i.test(message)) return "连接测试超时,请检查请求地址、网络或模型名称";
|
|
19
|
+
if (/invalid.*(?:api )?key|unauthorized|\b401\b/i.test(message)) return "API Key 无效或未授权";
|
|
20
|
+
if (/model.*not found|\b404\b/i.test(message)) return "模型名称或请求地址不存在";
|
|
21
|
+
return message;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function showList(message = "", tone = "") {
|
|
25
|
+
moduleBar.classList.remove("is-hidden");
|
|
26
|
+
profileList.classList.remove("is-hidden");
|
|
27
|
+
profileEditor.classList.add("is-hidden");
|
|
28
|
+
profileList.busy = false;
|
|
29
|
+
setListStatus(message, tone);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function showEditor(profile) {
|
|
33
|
+
moduleBar.classList.add("is-hidden");
|
|
34
|
+
profileList.classList.add("is-hidden");
|
|
35
|
+
profileEditor.classList.remove("is-hidden");
|
|
36
|
+
profileEditor.profile = profile || null;
|
|
37
|
+
profileEditor.setBusy(false);
|
|
38
|
+
profileEditor.setStatus("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function refresh() {
|
|
42
|
+
profileList.profiles = await client.listProfiles();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function persist(profile, credential) {
|
|
46
|
+
const saved = await client.saveProfile(profile);
|
|
47
|
+
if (credential) await client.setCredential(saved.id, credential);
|
|
48
|
+
return saved;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addProfile.addEventListener("click", () => showEditor(null));
|
|
52
|
+
profileList.addEventListener("profile-edit", (event) => showEditor(event.detail.profile));
|
|
53
|
+
profileEditor.addEventListener("profile-cancel", () => showList());
|
|
54
|
+
|
|
55
|
+
profileEditor.addEventListener("profile-save", async (event) => {
|
|
56
|
+
profileEditor.setBusy(true, "保存中…");
|
|
57
|
+
try {
|
|
58
|
+
await persist(event.detail.profile, event.detail.credential);
|
|
59
|
+
await refresh();
|
|
60
|
+
profileEditor.setBusy(false);
|
|
61
|
+
showList("已保存,配置现在可以使用", "good");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
profileEditor.setBusy(false);
|
|
64
|
+
profileEditor.setStatus(error.message, "bad");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
profileEditor.addEventListener("profile-test", async (event) => {
|
|
69
|
+
profileEditor.setBusy(true, "测试中…");
|
|
70
|
+
try {
|
|
71
|
+
const result = await client.testProfileDraft(event.detail.profile, event.detail.credential);
|
|
72
|
+
profileEditor.setBusy(false);
|
|
73
|
+
profileEditor.setStatus(
|
|
74
|
+
result.runtimeVerified ? `连接成功 · ${result.model || event.detail.profile.models.default} · ${result.durationMs}ms;请点击保存后生效` : "连接测试失败",
|
|
75
|
+
result.runtimeVerified ? "good" : "bad",
|
|
76
|
+
);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
profileEditor.setBusy(false);
|
|
79
|
+
profileEditor.setStatus(testErrorMessage(error), "bad");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
profileList.addEventListener("profile-test", async (event) => {
|
|
84
|
+
profileList.busy = true;
|
|
85
|
+
setListStatus("测试中…");
|
|
86
|
+
try {
|
|
87
|
+
const result = await client.testProfileDraft(event.detail.profile, "");
|
|
88
|
+
profileList.busy = false;
|
|
89
|
+
setListStatus(result.runtimeVerified ? `连接成功 · ${result.durationMs}ms` : "连接测试失败", result.runtimeVerified ? "good" : "bad");
|
|
90
|
+
} catch (error) {
|
|
91
|
+
profileList.busy = false;
|
|
92
|
+
setListStatus(testErrorMessage(error), "bad");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
profileList.addEventListener("profile-delete", async (event) => {
|
|
97
|
+
if (!window.confirm(`删除“${event.detail.profile.name}”?`)) return;
|
|
98
|
+
profileList.busy = true;
|
|
99
|
+
setListStatus("删除中…");
|
|
100
|
+
try {
|
|
101
|
+
await client.deleteProfile(event.detail.profile.id);
|
|
102
|
+
await refresh();
|
|
103
|
+
profileList.busy = false;
|
|
104
|
+
setListStatus("已删除", "good");
|
|
105
|
+
} catch (error) {
|
|
106
|
+
profileList.busy = false;
|
|
107
|
+
setListStatus(error.message, "bad");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
window.addEventListener("beforeunload", () => client.destroy());
|
|
112
|
+
refresh().catch((error) => setListStatus(error.message, "bad"));
|
|
113
|
+
})();
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# Minimal Test Host
|
|
2
|
-
|
|
3
|
-
Start the real Workbench Server from the repository root. The test host uses the
|
|
4
|
-
same stable product data directory as an installed Workbench instance, rather
|
|
5
|
-
than a disposable temporary directory:
|
|
6
|
-
|
|
7
|
-
```powershell
|
|
8
|
-
npm run start:test-host
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Then open `http://127.0.0.1:5762/test-host/`. Profile metadata is stored in
|
|
12
|
-
`profiles.v2.json`; credentials are stored separately in `credentials.v1.json`
|
|
13
|
-
under that product data directory. The default mode is `REAL` and an unavailable
|
|
14
|
-
server/runtime is shown as a failure. Fault injection is loaded only after the
|
|
15
|
-
explicit button is pressed and reports are marked `MOCK`.
|
|
1
|
+
# Minimal Test Host
|
|
2
|
+
|
|
3
|
+
Start the real Workbench Server from the repository root. The test host uses the
|
|
4
|
+
same stable product data directory as an installed Workbench instance, rather
|
|
5
|
+
than a disposable temporary directory:
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
npm run start:test-host
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then open `http://127.0.0.1:5762/test-host/`. Profile metadata is stored in
|
|
12
|
+
`profiles.v2.json`; credentials are stored separately in `credentials.v1.json`
|
|
13
|
+
under that product data directory. The default mode is `REAL` and an unavailable
|
|
14
|
+
server/runtime is shown as a failure. Fault injection is loaded only after the
|
|
15
|
+
explicit button is pressed and reports are marked `MOCK`.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
:root{color-scheme:light;--bg:#eaf0f5;--panel:#fff;--panel2:#f6f8fb;--line:#d7e0e9;--text:#152235;--muted:#65758a;--accent:#4f6f8f;--good:#2f7d64;--bad:#ad4f4f}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif}.app{height:100vh;display:grid;grid-template-rows:auto minmax(0,1fr)}header{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:14px 18px;border-bottom:1px solid var(--line);background:var(--panel)}h1{margin:0;font-size:18px}header p{margin:4px 0 0;color:var(--muted);font-size:12px}.badges{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.badge,button,select{border:1px solid var(--line);border-radius:8px;background:var(--panel2);padding:7px 9px;font:inherit;font-size:12px}.badge{border-radius:999px}.badge.ok,.real{color:var(--good)}.badge.err{color:var(--bad)}button{cursor:pointer}.primary{border-color:transparent;background:var(--accent);color:#fff}.layout{min-height:0;display:grid;grid-template-columns:220px minmax(410px,1fr) 330px;gap:12px;padding:12px}.panel{min-height:0;overflow:hidden;border:1px solid var(--line);border-radius:13px;background:var(--panel)}.panel-title{display:flex;align-items:center;justify-content:space-between;padding:11px 12px;border-bottom:1px solid var(--line);font-size:13px;font-weight:750}.panel-body{height:calc(100% - 44px);padding:10px;overflow:auto}.context{width:100%;margin-bottom:7px;padding:9px;text-align:left}.context.active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(79,111,143,.12)}.context small{display:block;margin-top:4px;color:var(--muted)}label{display:grid;gap:5px;margin-top:13px;color:var(--muted);font-size:11px}select{width:100%;background:#fff}.button-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:14px}.wide{grid-column:1/-1}.hint{color:var(--muted);font-size:11px;line-height:1.55}.workbench{min-height:0}dl{margin:0}.metric{display:grid;grid-template-columns:1fr auto;gap:8px;padding:6px 0;border-bottom:1px solid var(--line);font-size:11px}.metric dt{color:var(--muted)}.metric dd{margin:0;font-weight:700;max-width:180px;overflow:hidden;text-overflow:ellipsis}h3{margin:14px 0 7px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}pre{min-height:120px;max-height:220px;margin:0;overflow:auto;padding:8px;border:1px solid var(--line);border-radius:8px;background:#101923;color:#dce8f4;white-space:pre-wrap;font:10px/1.5 Consolas,monospace}.drawer{position:fixed;inset:0;display:none;padding:4vh 4vw;background:rgba(15,23,42,.42);z-index:30}.drawer.open{display:block}.drawer-box{position:relative;height:92vh;border-radius:13px;background:var(--panel);overflow:hidden}.drawer-box>button{position:absolute;right:10px;top:9px;z-index:5}@media(max-width:1150px){.layout{grid-template-columns:210px 1fr}.inspector{display:none}}@media(max-width:760px){.app{height:auto;min-height:100vh}.layout{display:block}.sidebar{margin-bottom:10px}.workbench{height:680px}header{align-items:flex-start;flex-direction:column}}
|
|
2
|
-
.drawer-box{display:grid;grid-template-rows:52px minmax(0,1fr)}.drawer-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 12px 0 16px;border-bottom:1px solid var(--line);background:var(--panel)}.drawer-head strong{font-size:13px}.drawer-content{min-height:0;overflow:auto;padding:12px}@media(max-width:760px){.drawer{padding:0}.drawer-box{height:100vh;border-radius:0}}
|
|
1
|
+
:root{color-scheme:light;--bg:#eaf0f5;--panel:#fff;--panel2:#f6f8fb;--line:#d7e0e9;--text:#152235;--muted:#65758a;--accent:#4f6f8f;--good:#2f7d64;--bad:#ad4f4f}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif}.app{height:100vh;display:grid;grid-template-rows:auto minmax(0,1fr)}header{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:14px 18px;border-bottom:1px solid var(--line);background:var(--panel)}h1{margin:0;font-size:18px}header p{margin:4px 0 0;color:var(--muted);font-size:12px}.badges{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap}.badge,button,select{border:1px solid var(--line);border-radius:8px;background:var(--panel2);padding:7px 9px;font:inherit;font-size:12px}.badge{border-radius:999px}.badge.ok,.real{color:var(--good)}.badge.err{color:var(--bad)}button{cursor:pointer}.primary{border-color:transparent;background:var(--accent);color:#fff}.layout{min-height:0;display:grid;grid-template-columns:220px minmax(410px,1fr) 330px;gap:12px;padding:12px}.panel{min-height:0;overflow:hidden;border:1px solid var(--line);border-radius:13px;background:var(--panel)}.panel-title{display:flex;align-items:center;justify-content:space-between;padding:11px 12px;border-bottom:1px solid var(--line);font-size:13px;font-weight:750}.panel-body{height:calc(100% - 44px);padding:10px;overflow:auto}.context{width:100%;margin-bottom:7px;padding:9px;text-align:left}.context.active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(79,111,143,.12)}.context small{display:block;margin-top:4px;color:var(--muted)}label{display:grid;gap:5px;margin-top:13px;color:var(--muted);font-size:11px}select{width:100%;background:#fff}.button-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:14px}.wide{grid-column:1/-1}.hint{color:var(--muted);font-size:11px;line-height:1.55}.workbench{min-height:0}dl{margin:0}.metric{display:grid;grid-template-columns:1fr auto;gap:8px;padding:6px 0;border-bottom:1px solid var(--line);font-size:11px}.metric dt{color:var(--muted)}.metric dd{margin:0;font-weight:700;max-width:180px;overflow:hidden;text-overflow:ellipsis}h3{margin:14px 0 7px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}pre{min-height:120px;max-height:220px;margin:0;overflow:auto;padding:8px;border:1px solid var(--line);border-radius:8px;background:#101923;color:#dce8f4;white-space:pre-wrap;font:10px/1.5 Consolas,monospace}.drawer{position:fixed;inset:0;display:none;padding:4vh 4vw;background:rgba(15,23,42,.42);z-index:30}.drawer.open{display:block}.drawer-box{position:relative;height:92vh;border-radius:13px;background:var(--panel);overflow:hidden}.drawer-box>button{position:absolute;right:10px;top:9px;z-index:5}@media(max-width:1150px){.layout{grid-template-columns:210px 1fr}.inspector{display:none}}@media(max-width:760px){.app{height:auto;min-height:100vh}.layout{display:block}.sidebar{margin-bottom:10px}.workbench{height:680px}header{align-items:flex-start;flex-direction:column}}
|
|
2
|
+
.drawer-box{display:grid;grid-template-rows:52px minmax(0,1fr)}.drawer-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 12px 0 16px;border-bottom:1px solid var(--line);background:var(--panel)}.drawer-head strong{font-size:13px}.drawer-content{min-height:0;overflow:auto;padding:12px}@media(max-width:760px){.drawer{padding:0}.drawer-box{height:100vh;border-radius:0}}
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
(function () {
|
|
2
|
-
'use strict';
|
|
3
|
-
const contexts=[
|
|
4
|
-
{contextId:'context-a',projectId:'context-a',contextType:'test',displayName:'Context A',projectName:'Context A',workspaceRef:'test-workspace-a'},
|
|
5
|
-
{contextId:'context-b',projectId:'context-b',contextType:'test',displayName:'Context B',projectName:'Context B',workspaceRef:'test-workspace-b'},
|
|
6
|
-
{contextId:'context-c',projectId:'context-c',contextType:'test',displayName:'Context C',projectName:'Context C',workspaceRef:'test-workspace-c'},
|
|
7
|
-
];
|
|
8
|
-
const apiBase=new URLSearchParams(location.search).get('api')||`${location.origin}/api/claude-workbench/v1`;
|
|
9
|
-
const realClient=window.createClaudeWorkbenchClient({apiBase});
|
|
10
|
-
let activeClient=realClient,current=contexts[0],workbench=null,lastState=null,mounted=0,mode='REAL',lastReport=null;
|
|
11
|
-
const eventLog=document.querySelector('#eventLog');
|
|
12
|
-
const originalSubscribe=realClient.subscribe.bind(realClient);
|
|
13
|
-
realClient.subscribe=(sessionId,onEvent,onError,options)=>originalSubscribe(sessionId,event=>{log('SSE',event);onEvent(event)},onError,options);
|
|
14
|
-
const $=selector=>document.querySelector(selector);
|
|
15
|
-
function log(label,value){const row=`[${new Date().toLocaleTimeString()}] ${label} ${value?JSON.stringify(value):''}`;eventLog.textContent=`${row}\n${eventLog.textContent}`.slice(0,50000)}
|
|
16
|
-
function metric(name,value){return `<div class="metric"><dt>${name}</dt><dd>${value==null?'—':String(value)}</dd></div>`}
|
|
17
|
-
function profileTestError(error){const message=String(error?.message||'连接测试失败');if(/timed out|timeout/i.test(message))return'连接测试超时,请检查请求地址、网络或模型名称';if(/invalid.*(?:api )?key|unauthorized|\b401\b/i.test(message))return'API Key 无效或未授权';if(/model.*not found|\b404\b/i.test(message))return'模型名称或请求地址不存在';return message}
|
|
18
|
-
async function persistProfile(profile,credential){const saved=await realClient.saveProfile(profile);if(credential)await realClient.setCredential(saved.id,credential);return saved}
|
|
19
|
-
function renderMetrics(){const counts=activeClient.resourceCounts?.()||{};const state=lastState||{};$('#metrics').innerHTML=[metric('Mode',mode),metric('Context',current.contextId),metric('Generation',state.contextGeneration??state.contextVersion),metric('Session',state.sessionId),metric('Connection',state.session?.connectionState),metric('State',state.session?.state),metric('Last sequence',state.lastSequence),metric('Lines',state.lines?.length||0),metric('Permission',state.permission?.requestId),metric('Token used',state.tokenUsage?.used||0),metric('Open SSE',counts.streams||0),metric('Pending HTTP',counts.requests||0),metric('Reconnects',counts.reconnects||0),metric('Mounted',mounted)].join('')}
|
|
20
|
-
function selectedContext(){return{...current,aiProfileId:$('#profileSelect').value||undefined}}
|
|
21
|
-
function mount(){if(workbench)return;workbench=document.createElement('claude-workbench-pane');workbench.adapter=activeClient;workbench.context=selectedContext();workbench.addEventListener('workbench-state-change',event=>{lastState=event.detail;renderMetrics()});workbench.addEventListener('workbench-error',event=>log('ERROR',{message:event.detail.error.message}));workbench.addEventListener('open-profile-settings',openProfiles);$('#workbenchMount').appendChild(workbench);mounted++;log('MOUNT',selectedContext());renderMetrics()}
|
|
22
|
-
function unmount(){if(!workbench)return;workbench.destroy();workbench.remove();workbench=null;lastState=null;mounted=Math.max(0,mounted-1);log('UNMOUNT');renderMetrics()}
|
|
23
|
-
function selectContext(next){current=next;document.querySelectorAll('.context').forEach(button=>button.classList.toggle('active',button.dataset.id===next.contextId));if(workbench)workbench.context=selectedContext();log('CONTEXT',next);renderMetrics()}
|
|
24
|
-
async function refresh(){try{const [health,runtime,profiles]=await Promise.all([realClient.health(),realClient.runtime(),realClient.listProfiles()]);$('#serverBadge').textContent=`Server ${health.version}`;$('#serverBadge').className='badge ok';$('#runtimeBadge').textContent=runtime.available?'Runtime available':'Runtime unavailable';$('#runtimeBadge').className=runtime.available?'badge ok':'badge err';const old=$('#profileSelect').value;$('#profileSelect').innerHTML='<option value="">选择 Profile</option>'+profiles.map(p=>`<option value="${p.id}">${p.name||p.id}${p.credentialConfigured?'':' · no key'}</option>`).join('');if(profiles.some(p=>p.id===old))$('#profileSelect').value=old;else if(profiles[0])$('#profileSelect').value=profiles[0].id;$('#profileSettings').profiles=profiles;log('HEALTH',{health,runtime,profiles:profiles.length})}catch(error){$('#serverBadge').textContent='Server offline';$('#serverBadge').className='badge err';log('HEALTH ERROR',{message:error.message})}}
|
|
25
|
-
async function stress(){for(let index=0;index<20;index++){selectContext(contexts[index%2]);await new Promise(resolve=>setTimeout(resolve,40))}log('STRESS COMPLETE',activeClient.resourceCounts?.())}
|
|
26
|
-
async function acceptance(){const startedAt=new Date().toISOString(),checks=[];const check=async(name,fn)=>{try{const detail=await fn();checks.push({name,ok:true,detail})}catch(error){checks.push({name,ok:false,error:error.message})}};await check('server-health',()=>realClient.health());await check('runtime-available',async()=>{const value=await realClient.runtime();if(!value.available)throw new Error('Claude Code runtime unavailable');return value});await check('profile-selected',async()=>{if(!$('#profileSelect').value)throw new Error('Select an AI Profile');return $('#profileSelect').value});await check('profile-real-runtime',async()=>{const result=await realClient.testProfile($('#profileSelect').value);if(!result.runtimeVerified)throw new Error('runtimeVerified=false');return result});await check('context-generation-isolation',async()=>{await stress();if(lastState&&lastState.context?.contextId!==current.contextId)throw new Error('late context state won');return{contextId:current.contextId,generation:lastState?.contextGeneration}});await check('lifecycle-cleanup',async()=>{unmount();await new Promise(resolve=>setTimeout(resolve,20));const counts=activeClient.resourceCounts();if(counts.streams||counts.requests)throw new Error(`resources remain: ${JSON.stringify(counts)}`);mount();return counts});lastReport={mode,serverVersion:($('#serverBadge').textContent||'').replace('Server ',''),runtime:'claude-code',profileId:$('#profileSelect').value||null,checks,resourceLeaks:checks.filter(item=>item.name==='lifecycle-cleanup'&&!item.ok),startedAt,finishedAt:new Date().toISOString()};$('#reportLog').textContent=JSON.stringify(lastReport,null,2);log('ACCEPTANCE',lastReport)}
|
|
27
|
-
function exportReport(){if(!lastReport)return;const blob=new Blob([JSON.stringify(lastReport,null,2)],{type:'application/json'});const link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=`claude-workbench-${mode.toLowerCase()}-report.json`;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000)}
|
|
28
|
-
function openProfiles(){const drawer=$('#profileDrawer');drawer.classList.add('open');drawer.setAttribute('aria-hidden','false');refresh()}
|
|
29
|
-
async function toggleMock(){if(mode==='MOCK'){unmount();activeClient.destroy?.();activeClient=realClient;mode='REAL';$('#modeBadge').textContent='REAL';$('#mockBtn').textContent='Enable Optional Fault Injection';mount();renderMetrics();return}if(!window.createFaultInjectionAdapter){await new Promise((resolve,reject)=>{const script=document.createElement('script');script.src='optional-mock/fault-injection.js';script.onload=resolve;script.onerror=reject;document.head.appendChild(script)})}unmount();activeClient=window.createFaultInjectionAdapter({onRaw:event=>log('MOCK',event)});mode='MOCK';$('#modeBadge').textContent='MOCK';$('#mockBtn').textContent='Disable Fault Injection';mount();renderMetrics()}
|
|
30
|
-
const profileSettings=$('#profileSettings');
|
|
31
|
-
profileSettings.addEventListener('profile-save',async event=>{profileSettings.setBusy(true,'保存中…');try{const saved=await persistProfile(event.detail.profile,event.detail.credential);await refresh();profileSettings.setBusy(false);profileSettings.closeEditor();profileSettings.setStatus('已保存,配置现在可以使用','good');log('PROFILE SAVE',{id:saved.id})}catch(error){profileSettings.setBusy(false);profileSettings.setStatus(error.message,'bad');log('PROFILE SAVE ERROR',{message:error.message})}});
|
|
32
|
-
profileSettings.addEventListener('profile-delete',async event=>{if(!window.confirm(`删除“${event.detail.profile.name}”?`))return;profileSettings.setBusy(true,'删除中…');try{await realClient.deleteProfile(event.detail.profile.id);await refresh();profileSettings.setBusy(false);profileSettings.setStatus('已删除','good');log('PROFILE DELETE',{id:event.detail.profile.id})}catch(error){profileSettings.setBusy(false);profileSettings.setStatus(error.message,'bad');log('PROFILE DELETE ERROR',{message:error.message})}});
|
|
33
|
-
profileSettings.addEventListener('profile-test',async event=>{profileSettings.setBusy(true,'测试中…');try{const result=await realClient.testProfileDraft(event.detail.profile,event.detail.credential);profileSettings.setBusy(false);profileSettings.setStatus(result.runtimeVerified?`连接成功 · ${result.model||event.detail.profile.models.default} · ${result.durationMs}ms;请点击保存后生效`:'连接测试失败',result.runtimeVerified?'good':'bad');log('REAL PROFILE TEST',result)}catch(error){const message=profileTestError(error);profileSettings.setBusy(false);profileSettings.setStatus(message,'bad');log('PROFILE TEST ERROR',{message})}});
|
|
34
|
-
$('#contexts').innerHTML=contexts.map((c,index)=>`<button class="context ${index===0?'active':''}" data-id="${c.contextId}"><strong>${c.displayName}</strong><small>${c.workspaceRef}</small></button>`).join('');$('#contexts').onclick=event=>{const button=event.target.closest('.context');if(button)selectContext(contexts.find(item=>item.contextId===button.dataset.id))};$('#mountBtn').onclick=mount;$('#unmountBtn').onclick=unmount;$('#remountBtn').onclick=()=>{unmount();mount()};$('#stressBtn').onclick=stress;$('#refreshBtn').onclick=refresh;$('#profilesBtn').onclick=openProfiles;$('#closeProfiles').onclick=()=>{const drawer=$('#profileDrawer');drawer.classList.remove('open');drawer.setAttribute('aria-hidden','true')};$('#acceptBtn').onclick=acceptance;$('#clearBtn').onclick=()=>eventLog.textContent='';$('#exportBtn').onclick=exportReport;$('#mockBtn').onclick=toggleMock;$('#profileSelect').onchange=()=>{if(workbench)workbench.context=selectedContext()};refresh().then(mount);setInterval(renderMetrics,500);
|
|
35
|
-
})();
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
const contexts=[
|
|
4
|
+
{contextId:'context-a',projectId:'context-a',contextType:'test',displayName:'Context A',projectName:'Context A',workspaceRef:'test-workspace-a'},
|
|
5
|
+
{contextId:'context-b',projectId:'context-b',contextType:'test',displayName:'Context B',projectName:'Context B',workspaceRef:'test-workspace-b'},
|
|
6
|
+
{contextId:'context-c',projectId:'context-c',contextType:'test',displayName:'Context C',projectName:'Context C',workspaceRef:'test-workspace-c'},
|
|
7
|
+
];
|
|
8
|
+
const apiBase=new URLSearchParams(location.search).get('api')||`${location.origin}/api/claude-workbench/v1`;
|
|
9
|
+
const realClient=window.createClaudeWorkbenchClient({apiBase});
|
|
10
|
+
let activeClient=realClient,current=contexts[0],workbench=null,lastState=null,mounted=0,mode='REAL',lastReport=null;
|
|
11
|
+
const eventLog=document.querySelector('#eventLog');
|
|
12
|
+
const originalSubscribe=realClient.subscribe.bind(realClient);
|
|
13
|
+
realClient.subscribe=(sessionId,onEvent,onError,options)=>originalSubscribe(sessionId,event=>{log('SSE',event);onEvent(event)},onError,options);
|
|
14
|
+
const $=selector=>document.querySelector(selector);
|
|
15
|
+
function log(label,value){const row=`[${new Date().toLocaleTimeString()}] ${label} ${value?JSON.stringify(value):''}`;eventLog.textContent=`${row}\n${eventLog.textContent}`.slice(0,50000)}
|
|
16
|
+
function metric(name,value){return `<div class="metric"><dt>${name}</dt><dd>${value==null?'—':String(value)}</dd></div>`}
|
|
17
|
+
function profileTestError(error){const message=String(error?.message||'连接测试失败');if(/timed out|timeout/i.test(message))return'连接测试超时,请检查请求地址、网络或模型名称';if(/invalid.*(?:api )?key|unauthorized|\b401\b/i.test(message))return'API Key 无效或未授权';if(/model.*not found|\b404\b/i.test(message))return'模型名称或请求地址不存在';return message}
|
|
18
|
+
async function persistProfile(profile,credential){const saved=await realClient.saveProfile(profile);if(credential)await realClient.setCredential(saved.id,credential);return saved}
|
|
19
|
+
function renderMetrics(){const counts=activeClient.resourceCounts?.()||{};const state=lastState||{};$('#metrics').innerHTML=[metric('Mode',mode),metric('Context',current.contextId),metric('Generation',state.contextGeneration??state.contextVersion),metric('Session',state.sessionId),metric('Connection',state.session?.connectionState),metric('State',state.session?.state),metric('Last sequence',state.lastSequence),metric('Lines',state.lines?.length||0),metric('Permission',state.permission?.requestId),metric('Token used',state.tokenUsage?.used||0),metric('Open SSE',counts.streams||0),metric('Pending HTTP',counts.requests||0),metric('Reconnects',counts.reconnects||0),metric('Mounted',mounted)].join('')}
|
|
20
|
+
function selectedContext(){return{...current,aiProfileId:$('#profileSelect').value||undefined}}
|
|
21
|
+
function mount(){if(workbench)return;workbench=document.createElement('claude-workbench-pane');workbench.adapter=activeClient;workbench.context=selectedContext();workbench.addEventListener('workbench-state-change',event=>{lastState=event.detail;renderMetrics()});workbench.addEventListener('workbench-error',event=>log('ERROR',{message:event.detail.error.message}));workbench.addEventListener('open-profile-settings',openProfiles);$('#workbenchMount').appendChild(workbench);mounted++;log('MOUNT',selectedContext());renderMetrics()}
|
|
22
|
+
function unmount(){if(!workbench)return;workbench.destroy();workbench.remove();workbench=null;lastState=null;mounted=Math.max(0,mounted-1);log('UNMOUNT');renderMetrics()}
|
|
23
|
+
function selectContext(next){current=next;document.querySelectorAll('.context').forEach(button=>button.classList.toggle('active',button.dataset.id===next.contextId));if(workbench)workbench.context=selectedContext();log('CONTEXT',next);renderMetrics()}
|
|
24
|
+
async function refresh(){try{const [health,runtime,profiles]=await Promise.all([realClient.health(),realClient.runtime(),realClient.listProfiles()]);$('#serverBadge').textContent=`Server ${health.version}`;$('#serverBadge').className='badge ok';$('#runtimeBadge').textContent=runtime.available?'Runtime available':'Runtime unavailable';$('#runtimeBadge').className=runtime.available?'badge ok':'badge err';const old=$('#profileSelect').value;$('#profileSelect').innerHTML='<option value="">选择 Profile</option>'+profiles.map(p=>`<option value="${p.id}">${p.name||p.id}${p.credentialConfigured?'':' · no key'}</option>`).join('');if(profiles.some(p=>p.id===old))$('#profileSelect').value=old;else if(profiles[0])$('#profileSelect').value=profiles[0].id;$('#profileSettings').profiles=profiles;log('HEALTH',{health,runtime,profiles:profiles.length})}catch(error){$('#serverBadge').textContent='Server offline';$('#serverBadge').className='badge err';log('HEALTH ERROR',{message:error.message})}}
|
|
25
|
+
async function stress(){for(let index=0;index<20;index++){selectContext(contexts[index%2]);await new Promise(resolve=>setTimeout(resolve,40))}log('STRESS COMPLETE',activeClient.resourceCounts?.())}
|
|
26
|
+
async function acceptance(){const startedAt=new Date().toISOString(),checks=[];const check=async(name,fn)=>{try{const detail=await fn();checks.push({name,ok:true,detail})}catch(error){checks.push({name,ok:false,error:error.message})}};await check('server-health',()=>realClient.health());await check('runtime-available',async()=>{const value=await realClient.runtime();if(!value.available)throw new Error('Claude Code runtime unavailable');return value});await check('profile-selected',async()=>{if(!$('#profileSelect').value)throw new Error('Select an AI Profile');return $('#profileSelect').value});await check('profile-real-runtime',async()=>{const result=await realClient.testProfile($('#profileSelect').value);if(!result.runtimeVerified)throw new Error('runtimeVerified=false');return result});await check('context-generation-isolation',async()=>{await stress();if(lastState&&lastState.context?.contextId!==current.contextId)throw new Error('late context state won');return{contextId:current.contextId,generation:lastState?.contextGeneration}});await check('lifecycle-cleanup',async()=>{unmount();await new Promise(resolve=>setTimeout(resolve,20));const counts=activeClient.resourceCounts();if(counts.streams||counts.requests)throw new Error(`resources remain: ${JSON.stringify(counts)}`);mount();return counts});lastReport={mode,serverVersion:($('#serverBadge').textContent||'').replace('Server ',''),runtime:'claude-code',profileId:$('#profileSelect').value||null,checks,resourceLeaks:checks.filter(item=>item.name==='lifecycle-cleanup'&&!item.ok),startedAt,finishedAt:new Date().toISOString()};$('#reportLog').textContent=JSON.stringify(lastReport,null,2);log('ACCEPTANCE',lastReport)}
|
|
27
|
+
function exportReport(){if(!lastReport)return;const blob=new Blob([JSON.stringify(lastReport,null,2)],{type:'application/json'});const link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=`claude-workbench-${mode.toLowerCase()}-report.json`;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000)}
|
|
28
|
+
function openProfiles(){const drawer=$('#profileDrawer');drawer.classList.add('open');drawer.setAttribute('aria-hidden','false');refresh()}
|
|
29
|
+
async function toggleMock(){if(mode==='MOCK'){unmount();activeClient.destroy?.();activeClient=realClient;mode='REAL';$('#modeBadge').textContent='REAL';$('#mockBtn').textContent='Enable Optional Fault Injection';mount();renderMetrics();return}if(!window.createFaultInjectionAdapter){await new Promise((resolve,reject)=>{const script=document.createElement('script');script.src='optional-mock/fault-injection.js';script.onload=resolve;script.onerror=reject;document.head.appendChild(script)})}unmount();activeClient=window.createFaultInjectionAdapter({onRaw:event=>log('MOCK',event)});mode='MOCK';$('#modeBadge').textContent='MOCK';$('#mockBtn').textContent='Disable Fault Injection';mount();renderMetrics()}
|
|
30
|
+
const profileSettings=$('#profileSettings');
|
|
31
|
+
profileSettings.addEventListener('profile-save',async event=>{profileSettings.setBusy(true,'保存中…');try{const saved=await persistProfile(event.detail.profile,event.detail.credential);await refresh();profileSettings.setBusy(false);profileSettings.closeEditor();profileSettings.setStatus('已保存,配置现在可以使用','good');log('PROFILE SAVE',{id:saved.id})}catch(error){profileSettings.setBusy(false);profileSettings.setStatus(error.message,'bad');log('PROFILE SAVE ERROR',{message:error.message})}});
|
|
32
|
+
profileSettings.addEventListener('profile-delete',async event=>{if(!window.confirm(`删除“${event.detail.profile.name}”?`))return;profileSettings.setBusy(true,'删除中…');try{await realClient.deleteProfile(event.detail.profile.id);await refresh();profileSettings.setBusy(false);profileSettings.setStatus('已删除','good');log('PROFILE DELETE',{id:event.detail.profile.id})}catch(error){profileSettings.setBusy(false);profileSettings.setStatus(error.message,'bad');log('PROFILE DELETE ERROR',{message:error.message})}});
|
|
33
|
+
profileSettings.addEventListener('profile-test',async event=>{profileSettings.setBusy(true,'测试中…');try{const result=await realClient.testProfileDraft(event.detail.profile,event.detail.credential);profileSettings.setBusy(false);profileSettings.setStatus(result.runtimeVerified?`连接成功 · ${result.model||event.detail.profile.models.default} · ${result.durationMs}ms;请点击保存后生效`:'连接测试失败',result.runtimeVerified?'good':'bad');log('REAL PROFILE TEST',result)}catch(error){const message=profileTestError(error);profileSettings.setBusy(false);profileSettings.setStatus(message,'bad');log('PROFILE TEST ERROR',{message})}});
|
|
34
|
+
$('#contexts').innerHTML=contexts.map((c,index)=>`<button class="context ${index===0?'active':''}" data-id="${c.contextId}"><strong>${c.displayName}</strong><small>${c.workspaceRef}</small></button>`).join('');$('#contexts').onclick=event=>{const button=event.target.closest('.context');if(button)selectContext(contexts.find(item=>item.contextId===button.dataset.id))};$('#mountBtn').onclick=mount;$('#unmountBtn').onclick=unmount;$('#remountBtn').onclick=()=>{unmount();mount()};$('#stressBtn').onclick=stress;$('#refreshBtn').onclick=refresh;$('#profilesBtn').onclick=openProfiles;$('#closeProfiles').onclick=()=>{const drawer=$('#profileDrawer');drawer.classList.remove('open');drawer.setAttribute('aria-hidden','true')};$('#acceptBtn').onclick=acceptance;$('#clearBtn').onclick=()=>eventLog.textContent='';$('#exportBtn').onclick=exportReport;$('#mockBtn').onclick=toggleMock;$('#profileSelect').onchange=()=>{if(workbench)workbench.context=selectedContext()};refresh().then(mount);setInterval(renderMetrics,500);
|
|
35
|
+
})();
|