@sanqianx/claude-ai-workbench 0.13.2-rc.2 → 0.13.2-rc.3
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 +1 -1
- 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 +5 -4
- package/packages/server/bin/claude-workbench-server.js +9 -1
- package/packages/server/index.js +5 -4
- package/packages/server/lib/agent-registry.js +3 -0
- package/packages/server/lib/cli-session-recovery.js +28 -0
- package/packages/server/lib/drivers/opencode-driver.js +8 -2
- package/packages/server/lib/drivers/zcode-driver.js +63 -16
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -0
- package/packages/server/lib/workbench-service.js +3 -0
- package/packages/server/package.json +2 -2
- package/packages/ui/package.json +2 -2
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ npm run start:agent-terminal
|
|
|
62
62
|
- 数据目录:`~/.agent-terminal`(`AGENT_TERMINAL_DATA_DIR` 覆盖;端口 `AGENT_TERMINAL_PORT`)
|
|
63
63
|
- 首次使用:设置 → 模型供应商 → 添加供应商(Base URL / API Key / 模型列表),然后添加本地项目文件夹即可开聊。
|
|
64
64
|
- Agent 发现自动扫描本机:Claude Code(Agent SDK,支持 Anthropic 兼容端点如 MiniMax)、Codex(app-server,保留 CLI 回退)、OpenCode(serve + SSE,保留 CLI 回退)、ZCode(桌面内置 app-server)。版本探测成功不等于认证或任务执行成功,实际运行失败另写诊断日志。
|
|
65
|
-
- ZCode
|
|
65
|
+
- ZCode 原生认证使用已有桌面供应商配置,保留显式环境变量覆盖及个人配置;共享 Workbench 模型通过临时本机连接接入,上游 API Key 只在内存中,临时文件仅保存本机访问令牌,关闭时清理。不改写桌面安装资源或用户模型配置。同时支持旧版 runtimeModel 与新版 modelSelection 协议,失败和完成事件均结束当前任务。
|
|
66
66
|
- 零点击模型检测:填好 Base URL 与 API Key 后自动探测 `/v1/models` 并合并 models.dev 能力数据(上下文窗口、模态、推理),保存时自动填充模型列表与上下文大小。
|
|
67
67
|
- 会话内模型切换:切换供应商/模型不换会话,历史跨供应商延续(Claude 保留 resume,CLI 保留线程参数)。
|
|
68
68
|
- 运行时体验对齐:中断不再误报退出码错误、stderr 聚合为单个折叠、上下文用量按轮取最大值而非累计求和。
|
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.13.2-rc.
|
|
3
|
+
"version": "0.13.2-rc.3",
|
|
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.13.2-rc.
|
|
23
|
+
"@claude-ai-workbench/contracts": "0.13.2-rc.3"
|
|
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.13.2-rc.
|
|
3
|
+
"version": "0.13.2-rc.3",
|
|
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.13.2-rc.
|
|
23
|
+
"@claude-ai-workbench/contracts": "0.13.2-rc.3"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
|
@@ -29,12 +29,13 @@ app.server.listen(port, host, () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
let stopping = false;
|
|
32
|
-
function shutdown() {
|
|
32
|
+
async function shutdown() {
|
|
33
33
|
if (stopping) return; stopping = true;
|
|
34
|
-
|
|
34
|
+
setTimeout(() => process.exit(1), 10000).unref();
|
|
35
|
+
try { await app.service.destroy(); }
|
|
36
|
+
catch { process.stderr.write('SHUTDOWN_FAILED: 客户端运行环境未能完整关闭。\n'); process.exitCode = 1; }
|
|
35
37
|
app.server.closeAllConnections?.();
|
|
36
|
-
app.server.close(() => process.exit(0));
|
|
37
|
-
setTimeout(() => process.exit(0), 3000).unref();
|
|
38
|
+
app.server.close(() => process.exit(process.exitCode || 0));
|
|
38
39
|
}
|
|
39
40
|
process.on('SIGINT', shutdown);
|
|
40
41
|
process.on('SIGTERM', shutdown);
|
|
@@ -41,6 +41,14 @@ app.server.listen(port, host, () => {
|
|
|
41
41
|
warmFolderPicker({ title: '选择项目文件夹' }).catch(() => {});
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
let stopping = false;
|
|
45
|
+
async function shutdown() {
|
|
46
|
+
if (stopping) return; stopping = true;
|
|
47
|
+
setTimeout(() => process.exit(1), 10000).unref();
|
|
48
|
+
try { await app.service.destroy(); }
|
|
49
|
+
catch { process.stderr.write('SHUTDOWN_FAILED: 客户端运行环境未能完整关闭。\n'); process.exitCode = 1; }
|
|
50
|
+
app.server.closeAllConnections?.();
|
|
51
|
+
app.server.close(() => process.exit(process.exitCode || 0));
|
|
52
|
+
}
|
|
45
53
|
process.on('SIGINT', shutdown);
|
|
46
54
|
process.on('SIGTERM', shutdown);
|
package/packages/server/index.js
CHANGED
|
@@ -30,6 +30,7 @@ function createWorkbenchServer(options = {}) {
|
|
|
30
30
|
const apiPrefix = options.apiPrefix || DEFAULT_API_PREFIX;
|
|
31
31
|
const testRoot = path.join(dataDir, 'test-workspaces');
|
|
32
32
|
const projects = options.projects || new ProjectStore({ dataDir });
|
|
33
|
+
require('./lib/cli-session-recovery').recoverOwnedCliSessions({ dataDir, projects });
|
|
33
34
|
const sessionArchive = options.sessionArchive || new SessionArchiveStore({ dataDir });
|
|
34
35
|
const workspaces = {
|
|
35
36
|
'test-workspace-a': path.join(testRoot, 'context-a'),
|
|
@@ -62,9 +63,9 @@ function createWorkbenchServer(options = {}) {
|
|
|
62
63
|
drivers: {
|
|
63
64
|
'claude-code': claudeDriver,
|
|
64
65
|
...(options.drivers || {
|
|
65
|
-
codex: createCodexDriver(),
|
|
66
|
-
opencode: createOpenCodeDriver(),
|
|
67
|
-
zcode: createZCodeDriver(),
|
|
66
|
+
codex: createCodexDriver({ persistence: { dir: path.join(dataDir, 'cli-sessions') } }),
|
|
67
|
+
opencode: createOpenCodeDriver({ persistence: { dir: path.join(dataDir, 'cli-sessions') } }),
|
|
68
|
+
zcode: createZCodeDriver({ persistence: { dir: path.join(dataDir, 'cli-sessions') } }),
|
|
68
69
|
}),
|
|
69
70
|
},
|
|
70
71
|
});
|
|
@@ -136,7 +137,7 @@ function createWorkbenchServer(options = {}) {
|
|
|
136
137
|
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
137
138
|
res.end(JSON.stringify({ ok: false, error: 'not found' }));
|
|
138
139
|
});
|
|
139
|
-
server.on('close', () => service.destroy());
|
|
140
|
+
server.on('close', () => { Promise.resolve(service.destroy()).catch(error => diagnostics.error('agent.shutdown.failed', '客户端运行环境关闭失败。', { phase: 'shutdown', error: { code: error.code || 'SHUTDOWN_FAILED', message: error.message } })); });
|
|
140
141
|
return { server, service, profiles, contexts, attachments, projects, sessionArchive, registry, agentDiscovery, apiPrefix, dataDir };
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -46,6 +46,9 @@ class AgentRegistry {
|
|
|
46
46
|
asRunner() {
|
|
47
47
|
const registry = this;
|
|
48
48
|
return {
|
|
49
|
+
async destroy() {
|
|
50
|
+
await Promise.all([...registry.drivers.values()].map(driver => typeof driver.destroy === 'function' ? driver.destroy() : undefined));
|
|
51
|
+
},
|
|
49
52
|
startChatSession(args = {}) {
|
|
50
53
|
const agentId = normalizeAgentId(args.agentId);
|
|
51
54
|
const driver = registry.driverFor(agentId);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const SCHEMAS = new Set(['cli-json-runner-session/v1', 'codex-appserver-session/v1', 'opencode-serve-session/v1', 'zcode-appserver-session/v1']);
|
|
6
|
+
|
|
7
|
+
function recoverOwnedCliSessions({ dataDir, projects, sourceDir = path.join(os.homedir(), '.agent-terminal/cli-sessions') }) {
|
|
8
|
+
const targetDir = path.join(dataDir, 'cli-sessions');
|
|
9
|
+
if (path.resolve(sourceDir) === path.resolve(targetDir) || !fs.existsSync(sourceDir)) return;
|
|
10
|
+
const owners = new Map(projects.list().map(project => [project.id, path.resolve(project.rootPath)]));
|
|
11
|
+
// Earlier v5 wiring put CLI records in the standalone directory. Preserve
|
|
12
|
+
// only records whose project ID AND code directory belong to this product.
|
|
13
|
+
for (const name of fs.readdirSync(sourceDir)) {
|
|
14
|
+
if (!/^[\w.-]+\.json$/.test(name)) continue;
|
|
15
|
+
const source = path.join(sourceDir, name); const target = path.join(targetDir, name);
|
|
16
|
+
let record;
|
|
17
|
+
try {
|
|
18
|
+
const stat = fs.lstatSync(source);
|
|
19
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 8 * 1024 * 1024 || fs.existsSync(target)) continue;
|
|
20
|
+
record = JSON.parse(fs.readFileSync(source, 'utf8'));
|
|
21
|
+
} catch { continue; }
|
|
22
|
+
if (!SCHEMAS.has(record.schema) || !owners.has(record.projectSlug) || typeof record.projectPath !== 'string' || path.resolve(record.projectPath) !== owners.get(record.projectSlug) || `${record.sessionId}.json` !== name) continue;
|
|
23
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
24
|
+
try { fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL); }
|
|
25
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
module.exports = { recoverOwnedCliSessions };
|
|
@@ -493,6 +493,7 @@ function createServeOpenCodeDriver(options = {}) {
|
|
|
493
493
|
session.lastResult = '';
|
|
494
494
|
session.currentTurn = {
|
|
495
495
|
assistantMessageID: null,
|
|
496
|
+
assistantMessageIDs: new Set(),
|
|
496
497
|
userMessageIDs: new Set(), // echoed prompts never stream as output
|
|
497
498
|
parts: new Map(), // partID -> tracking state
|
|
498
499
|
sawEvent: false,
|
|
@@ -849,8 +850,11 @@ function createServeOpenCodeDriver(options = {}) {
|
|
|
849
850
|
return;
|
|
850
851
|
}
|
|
851
852
|
if (info.role !== 'assistant') return;
|
|
853
|
+
const turn = session.currentTurn;
|
|
854
|
+
if (info.id && !turn.assistantMessageIDs.has(info.id) && info.time?.created && info.time.created < turn.startedAt - 5000) return;
|
|
852
855
|
markActivity(session);
|
|
853
|
-
|
|
856
|
+
// Tool rounds and the final answer have distinct assistant message IDs.
|
|
857
|
+
if (info.id) { turn.assistantMessageIDs.add(info.id); turn.assistantMessageID = info.id; }
|
|
854
858
|
// Completed assistant messages carry the final token totals; they back
|
|
855
859
|
// up the per-step-finish usage when a turn had no step-finish parts.
|
|
856
860
|
if (info.time && info.time.completed && info.tokens && info.tokens.total != null) {
|
|
@@ -863,6 +867,7 @@ function createServeOpenCodeDriver(options = {}) {
|
|
|
863
867
|
// the assistant message this turn belongs to.
|
|
864
868
|
function partBelongsToTurn(turn, messageID) {
|
|
865
869
|
if (messageID && turn.userMessageIDs.has(messageID)) return false;
|
|
870
|
+
if (messageID && turn.assistantMessageIDs.has(messageID)) return true;
|
|
866
871
|
if (turn.assistantMessageID && messageID && messageID !== turn.assistantMessageID) return false;
|
|
867
872
|
return true;
|
|
868
873
|
}
|
|
@@ -872,6 +877,7 @@ function createServeOpenCodeDriver(options = {}) {
|
|
|
872
877
|
if (!partBelongsToTurn(session.currentTurn, part.messageID)) return;
|
|
873
878
|
if (!session.currentTurn.assistantMessageID && part.messageID) {
|
|
874
879
|
session.currentTurn.assistantMessageID = part.messageID;
|
|
880
|
+
session.currentTurn.assistantMessageIDs.add(part.messageID);
|
|
875
881
|
}
|
|
876
882
|
markActivity(session);
|
|
877
883
|
if (part.type === 'step-start') {
|
|
@@ -961,7 +967,7 @@ function createServeOpenCodeDriver(options = {}) {
|
|
|
961
967
|
const turn = session.currentTurn;
|
|
962
968
|
if (!turn || field !== 'text' || !partID) return;
|
|
963
969
|
if (!partBelongsToTurn(turn, messageID)) return;
|
|
964
|
-
if (!turn.assistantMessageID && messageID) turn.assistantMessageID = messageID;
|
|
970
|
+
if (!turn.assistantMessageID && messageID) { turn.assistantMessageID = messageID; turn.assistantMessageIDs.add(messageID); }
|
|
965
971
|
markActivity(session);
|
|
966
972
|
const tracker = partTracker(session, partID);
|
|
967
973
|
if (!tracker) return;
|
|
@@ -234,6 +234,11 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
234
234
|
let nextId = 1;
|
|
235
235
|
let nextPermissionId = 1;
|
|
236
236
|
let bridge = null;
|
|
237
|
+
let protocolMode = null;
|
|
238
|
+
let profileRuntime = null;
|
|
239
|
+
let profileRuntimeSignature = null;
|
|
240
|
+
let bridgeSetup = Promise.resolve();
|
|
241
|
+
let destroyed = false;
|
|
237
242
|
let desktopProviderCache; // lazily-read ~/.zcode/v2 providers (agent-owned auth)
|
|
238
243
|
|
|
239
244
|
function summary(session) {
|
|
@@ -519,13 +524,15 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
519
524
|
// Invalid injected profiles (no base URL) throw here and surface as a
|
|
520
525
|
// turn error, mirroring the codex driver's bridgeSpecFor contract.
|
|
521
526
|
runtimeModel = runtimeModelFor(profile);
|
|
522
|
-
|
|
527
|
+
session.preparing = true;
|
|
528
|
+
session.runtimeSignature = profile?.apiKey ? runtimeModel.revision : 'agent-owned';
|
|
529
|
+
await ensureBridge(profile, session);
|
|
523
530
|
await ensureZcodeSession(session, runtimeModel);
|
|
524
531
|
} catch (error) {
|
|
525
532
|
emit(session, { type: 'claude/error', message: `zcode app-server unavailable: ${error.message}`.slice(0, 500) });
|
|
526
533
|
setState(session, 'idle');
|
|
527
534
|
return { started: false };
|
|
528
|
-
}
|
|
535
|
+
} finally { session.preparing = false; }
|
|
529
536
|
|
|
530
537
|
emit(session, {
|
|
531
538
|
type: 'claude/user-prompt',
|
|
@@ -544,11 +551,6 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
544
551
|
if (images.length) {
|
|
545
552
|
emitDiag(session, { type: 'claude/stderr', text: 'the zcode app-server driver does not support image attachments yet' });
|
|
546
553
|
}
|
|
547
|
-
await bridge.request('session/send', {
|
|
548
|
-
sessionId: session.zcodeSessionId,
|
|
549
|
-
content: appendTextAttachments(value, attachments) + (sendOptions.knowledgeContext || ''),
|
|
550
|
-
...(runtimeModel ? { runtimeModel } : {}),
|
|
551
|
-
}, { timeoutMs: 30000 });
|
|
552
554
|
session.currentTurn = {
|
|
553
555
|
turnId: null,
|
|
554
556
|
announcedText: new Set(),
|
|
@@ -558,11 +560,14 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
558
560
|
sawEvent: false,
|
|
559
561
|
};
|
|
560
562
|
session.turnPromise = new Promise(resolve => { session.currentTurn.resolve = resolve; });
|
|
563
|
+
await bridge.request('session/send', {
|
|
564
|
+
sessionId: session.zcodeSessionId,
|
|
565
|
+
content: appendTextAttachments(value, attachments) + (sendOptions.knowledgeContext || ''),
|
|
566
|
+
...(protocolMode === 'selection' ? (session.nativeModel ? { modelSelection: session.nativeModel } : {}) : runtimeModel ? { runtimeModel } : {}),
|
|
567
|
+
}, { timeoutMs: 30000 });
|
|
561
568
|
return { started: true };
|
|
562
569
|
} catch (error) {
|
|
563
|
-
|
|
564
|
-
session.currentTurn = null;
|
|
565
|
-
setState(session, 'idle');
|
|
570
|
+
finishTurn(session, { resultType: 'error', response: '' }, `zcode turn failed to start: ${error.message}`);
|
|
566
571
|
return { started: false };
|
|
567
572
|
}
|
|
568
573
|
},
|
|
@@ -650,6 +655,15 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
650
655
|
|
|
651
656
|
findClaudeExecutableForSdk() { return null; },
|
|
652
657
|
|
|
658
|
+
async destroy() {
|
|
659
|
+
destroyed = true;
|
|
660
|
+
for (const session of sessions.values()) if (ACTIVE_STATES.has(session.state)) runner.abort(session.sessionId);
|
|
661
|
+
await bridge?.close(); bridge = null;
|
|
662
|
+
await bridgeSetup.catch(() => {});
|
|
663
|
+
await bridge?.close(); bridge = null;
|
|
664
|
+
await profileRuntime?.close(); profileRuntime = null;
|
|
665
|
+
},
|
|
666
|
+
|
|
653
667
|
// Test seams.
|
|
654
668
|
get transportMode() { return 'app-server'; },
|
|
655
669
|
get bridgeState() { return bridge ? bridge.state : 'uncreated'; },
|
|
@@ -658,7 +672,22 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
658
672
|
|
|
659
673
|
// -- bridge lifecycle ----------------------------------------------------
|
|
660
674
|
|
|
661
|
-
function ensureBridge() {
|
|
675
|
+
function ensureBridge(profile, session) {
|
|
676
|
+
const preparing = bridgeSetup.then(() => prepareBridge(profile, session));
|
|
677
|
+
bridgeSetup = preparing.catch(() => {});
|
|
678
|
+
return preparing;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function prepareBridge(profile, session) {
|
|
682
|
+
if (destroyed) throw new Error('ZCode 工作台正在关闭。');
|
|
683
|
+
if (protocolMode === 'selection' && profileRuntimeSignature !== session.runtimeSignature) {
|
|
684
|
+
if ([...sessions.values()].some(other => other !== session && (other.preparing || ACTIVE_STATES.has(other.state)) && other.runtimeSignature !== session.runtimeSignature)) throw new Error('另一个 ZCode 任务正在使用不同的模型配置,请等待任务结束。');
|
|
685
|
+
if (profileRuntimeSignature !== null || profile?.apiKey) { await bridge?.close(); bridge = null; }
|
|
686
|
+
await profileRuntime?.close(); profileRuntime = null;
|
|
687
|
+
if (profile?.apiKey) profileRuntime = await require('./zcode-profile-runtime').createZCodeProfileRuntime(injectedRuntimeModel(profile));
|
|
688
|
+
profileRuntimeSignature = session.runtimeSignature;
|
|
689
|
+
}
|
|
690
|
+
if (destroyed) throw new Error('ZCode 工作台正在关闭。');
|
|
662
691
|
if (bridge && bridge.state === 'closed') { bridge = null; }
|
|
663
692
|
if (bridge && bridge.state === 'crashed') { recycleBridge('bridge exhausted its restart budget'); }
|
|
664
693
|
if (!bridge) {
|
|
@@ -667,7 +696,7 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
667
696
|
// runtime with the app-server stdio subcommand.
|
|
668
697
|
command: options.nodePath || process.execPath,
|
|
669
698
|
args: [cli.path, 'app-server', '--stdio'],
|
|
670
|
-
env: providerRuntimeEnvironment(cli.path, { env: options.env || process.env }),
|
|
699
|
+
env: { ...providerRuntimeEnvironment(cli.path, { env: options.env || process.env }), ...(profileRuntime ? { ZCODE_PERSONAL_PROVIDER_CONFIG_FILE: profileRuntime.file } : {}) },
|
|
671
700
|
spawn: options.appServerSpawn,
|
|
672
701
|
onEvent: handleNotification,
|
|
673
702
|
onServerRequest: handleServerRequest,
|
|
@@ -678,7 +707,12 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
678
707
|
log,
|
|
679
708
|
});
|
|
680
709
|
}
|
|
681
|
-
|
|
710
|
+
await bridge.start();
|
|
711
|
+
if (protocolMode === null) {
|
|
712
|
+
try { const capabilities = await bridge.request('runtime/capabilities', {}, { timeoutMs: 10000 }); protocolMode = capabilities?.independentPlanState === true ? 'selection' : 'runtime-model'; }
|
|
713
|
+
catch (error) { if (error.rpcCode !== -32601) throw error; protocolMode = 'runtime-model'; }
|
|
714
|
+
if (protocolMode === 'selection') return prepareBridge(profile, session);
|
|
715
|
+
}
|
|
682
716
|
}
|
|
683
717
|
|
|
684
718
|
function recycleBridge(reason) {
|
|
@@ -708,10 +742,11 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
708
742
|
async function ensureZcodeSession(session, runtimeModel) {
|
|
709
743
|
if (session.zcodeSessionId) {
|
|
710
744
|
try {
|
|
711
|
-
await bridge.request('session/resume', {
|
|
745
|
+
const resumed = await bridge.request('session/resume', {
|
|
712
746
|
sessionId: session.zcodeSessionId,
|
|
713
|
-
...(runtimeModel ? { runtimeModel } : {}),
|
|
747
|
+
...(protocolMode !== 'selection' && runtimeModel ? { runtimeModel } : {}),
|
|
714
748
|
}, { timeoutMs: 30000 });
|
|
749
|
+
if (protocolMode === 'selection') { session.nativeModel = resumed.settings?.model?.current || session.nativeModel || null; await selectNativeModel(session); }
|
|
715
750
|
zcodeSessions.set(session.zcodeSessionId, session.sessionId);
|
|
716
751
|
await subscribeZcodeSession(session);
|
|
717
752
|
return;
|
|
@@ -726,17 +761,28 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
726
761
|
const result = await bridge.request('session/create', {
|
|
727
762
|
workspace: { workspacePath: session.projectPath, workspaceKey: session.projectPath },
|
|
728
763
|
mode: MODE_FOR_PERMISSION[session.permissionMode] || 'build',
|
|
729
|
-
...(runtimeModel ? { runtimeModel } : {}),
|
|
764
|
+
...(protocolMode === 'selection' ? { titleGenerationEnabled: false } : runtimeModel ? { runtimeModel } : {}),
|
|
730
765
|
}, { timeoutMs: 60000 });
|
|
731
766
|
const zcodeId = result && result.session && (result.session.sessionId || result.session.id);
|
|
732
767
|
if (!zcodeId) throw new Error('zcode app-server did not return a session');
|
|
733
768
|
session.zcodeSessionId = zcodeId;
|
|
769
|
+
if (protocolMode === 'selection') { session.nativeModel = result.settings?.model?.current || null; await selectNativeModel(session); }
|
|
734
770
|
zcodeSessions.set(zcodeId, session.sessionId);
|
|
735
771
|
if (result.session && result.session.title && !session.title) session.title = result.session.title;
|
|
736
772
|
await subscribeZcodeSession(session);
|
|
737
773
|
persistSession(session);
|
|
738
774
|
}
|
|
739
775
|
|
|
776
|
+
async function selectNativeModel(session) {
|
|
777
|
+
if (profileRuntime) session.nativeModel = profileRuntime.selection;
|
|
778
|
+
else if (session.selectedModel) {
|
|
779
|
+
const slash = session.selectedModel.indexOf('/');
|
|
780
|
+
if (slash > 0) session.nativeModel = { providerId: session.selectedModel.slice(0, slash), modelId: session.selectedModel.slice(slash + 1), options: { reasoningLevel: 'enabled' } };
|
|
781
|
+
else if (session.nativeModel) session.nativeModel = { ...session.nativeModel, modelId: session.selectedModel };
|
|
782
|
+
}
|
|
783
|
+
await bridge.request('session/setMode', { sessionId: session.zcodeSessionId, mode: MODE_FOR_PERMISSION[session.permissionMode] || 'build' }, { timeoutMs: 10000 });
|
|
784
|
+
}
|
|
785
|
+
|
|
740
786
|
async function subscribeZcodeSession(session) {
|
|
741
787
|
await bridge.request('session/subscribe', {
|
|
742
788
|
sessionId: session.zcodeSessionId,
|
|
@@ -788,6 +834,7 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
788
834
|
case 'model.streaming': return onModelStreaming(session, payload || {});
|
|
789
835
|
case 'tool.updated': return onToolUpdated(session, payload || {});
|
|
790
836
|
case 'turn.completed': return finishTurn(session, payload || {});
|
|
837
|
+
case 'turn.failed': return finishTurn(session, { resultType: 'error', response: '' }, payload?.error?.message || payload?.error?.code || 'ZCode 任务失败');
|
|
791
838
|
case 'session.titleUpdated': {
|
|
792
839
|
if (payload && payload.title) session.title = String(payload.title).slice(0, 200);
|
|
793
840
|
return;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
const https = require('node:https');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { writeJson } = require('../json-file');
|
|
9
|
+
|
|
10
|
+
// ZCode 0.16.9 removed inline provider configuration. Its requestAuth override
|
|
11
|
+
// only applies to off-peak account models, not ordinary API-key providers.
|
|
12
|
+
// A private loopback transport keeps the upstream credential in memory; the
|
|
13
|
+
// native provider file contains only a short-lived local capability.
|
|
14
|
+
async function createZCodeProfileRuntime(spec) {
|
|
15
|
+
const upstream = new URL(spec.provider.baseURL);
|
|
16
|
+
if (!['http:', 'https:'].includes(upstream.protocol) || upstream.username || upstream.password || upstream.search || upstream.hash) throw new Error('ZCode 模型地址不支持内嵌凭据或查询参数。');
|
|
17
|
+
const key = spec.provider.apiKey.value;
|
|
18
|
+
const token = crypto.randomBytes(24).toString('hex');
|
|
19
|
+
const requests = new Set();
|
|
20
|
+
const prefix = `/${token}`;
|
|
21
|
+
const server = http.createServer((req, res) => {
|
|
22
|
+
const parsed = new URL(req.url, 'http://127.0.0.1');
|
|
23
|
+
const supplied = req.headers['x-api-key'] || String(req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
24
|
+
const suffix = parsed.pathname.slice(prefix.length);
|
|
25
|
+
if (supplied !== token || !parsed.pathname.startsWith(prefix + '/') || req.method !== 'POST' || !['/v1/messages', '/messages', '/chat/completions', '/v1/chat/completions', '/responses', '/v1/responses'].includes(suffix) || parsed.search) { req.resume(); res.writeHead(404); res.end(); return; }
|
|
26
|
+
const target = new URL(upstream);
|
|
27
|
+
target.pathname = upstream.pathname.replace(/\/$/, '') + suffix;
|
|
28
|
+
if (upstream.pathname.endsWith('/v1') && suffix.startsWith('/v1/')) target.pathname = upstream.pathname + suffix.slice(3);
|
|
29
|
+
const headers = { 'content-type': req.headers['content-type'] || 'application/json' };
|
|
30
|
+
for (const name of ['anthropic-version', 'anthropic-beta', 'content-length']) if (req.headers[name]) headers[name] = req.headers[name];
|
|
31
|
+
if (spec.provider.kind === 'anthropic') headers['x-api-key'] = key;
|
|
32
|
+
else headers.authorization = `Bearer ${key}`;
|
|
33
|
+
const remote = (target.protocol === 'https:' ? https : http).request(target, { method: 'POST', headers }, incoming => {
|
|
34
|
+
const allowed = {};
|
|
35
|
+
for (const name of ['content-type', 'cache-control', 'x-request-id', 'request-id']) if (incoming.headers[name]) allowed[name] = incoming.headers[name];
|
|
36
|
+
res.writeHead(incoming.statusCode, allowed); res.flushHeaders(); incoming.pipe(res);
|
|
37
|
+
});
|
|
38
|
+
requests.add(remote);
|
|
39
|
+
remote.once('close', () => requests.delete(remote));
|
|
40
|
+
remote.on('error', () => { if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' }); res.end('{"error":{"message":"模型连接失败"}}'); });
|
|
41
|
+
remote.setTimeout(300000, () => remote.destroy());
|
|
42
|
+
req.on('aborted', () => remote.destroy());
|
|
43
|
+
res.on('close', () => { if (!res.writableFinished) remote.destroy(); });
|
|
44
|
+
let size = 0;
|
|
45
|
+
req.on('data', bytes => { size += bytes.length; if (size > 8 * 1024 * 1024) { remote.destroy(); req.destroy(); } });
|
|
46
|
+
req.pipe(remote);
|
|
47
|
+
});
|
|
48
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
|
|
49
|
+
let root;
|
|
50
|
+
let file;
|
|
51
|
+
const selection = { ...spec.model, options: { reasoningLevel: 'enabled' } };
|
|
52
|
+
try {
|
|
53
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'workbench-zcode-runtime-'));
|
|
54
|
+
file = path.join(root, 'provider-config.json');
|
|
55
|
+
writeJson(file, { schemaVersion: 1, config: { providerOrder: [selection.providerId], defaultModelSelection: selection, providerConfigRules: { providerRules: [{ providerId: selection.providerId, providerName: 'Workbench', enabled: true, config: { group: 'standard-personal', access: { type: 'api-key', apiKey: token }, api: { type: spec.provider.kind === 'anthropic' ? 'anthropic-messages' : 'openai-chat-completions', baseUrl: `http://127.0.0.1:${server.address().port}${prefix}` }, personalModelIds: [selection.modelId] } }] }, modelConfigRules: { providerModelRules: [], manualProviderModelRules: [] } } });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
await new Promise(resolve => server.close(resolve));
|
|
58
|
+
if (file) { fs.rmSync(file, { force: true }); fs.rmSync(`${file}.${process.pid}.tmp`, { force: true }); }
|
|
59
|
+
if (root) { try { fs.rmdirSync(root); } catch {} }
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
let closed = false;
|
|
63
|
+
return { selection, file, async close() {
|
|
64
|
+
if (closed) return; closed = true;
|
|
65
|
+
for (const request of requests) request.destroy();
|
|
66
|
+
server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
|
|
67
|
+
fs.rmSync(file, { force: true });
|
|
68
|
+
try { fs.rmdirSync(root); } catch { /* never recursively remove native-created files */ }
|
|
69
|
+
} };
|
|
70
|
+
}
|
|
71
|
+
module.exports = { createZCodeProfileRuntime };
|
|
@@ -503,6 +503,7 @@ class WorkbenchService {
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
destroy() {
|
|
506
|
+
if (this.destroyPromise) return this.destroyPromise;
|
|
506
507
|
this.conversationCapture?.close();
|
|
507
508
|
if (this.archiveSweeper) { clearInterval(this.archiveSweeper); this.archiveSweeper = null; }
|
|
508
509
|
for (const sessionId of this.timers.keys()) this._clearTimeout(sessionId);
|
|
@@ -510,6 +511,8 @@ class WorkbenchService {
|
|
|
510
511
|
for (const session of this.runner.listSessions({})) {
|
|
511
512
|
if (ACTIVE_STATES.has(session.state)) try { this.runner.abort(session.sessionId); } catch {}
|
|
512
513
|
}
|
|
514
|
+
this.destroyPromise = Promise.resolve(this.runner.destroy?.());
|
|
515
|
+
return this.destroyPromise;
|
|
513
516
|
}
|
|
514
517
|
|
|
515
518
|
_runtimeProfile(profileId, selectedModel) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/server",
|
|
3
|
-
"version": "0.13.2-rc.
|
|
3
|
+
"version": "0.13.2-rc.3",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@anthropic-ai/claude-agent-sdk": "^0.2.141",
|
|
28
|
-
"@claude-ai-workbench/contracts": "0.13.2-rc.
|
|
28
|
+
"@claude-ai-workbench/contracts": "0.13.2-rc.3"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=18"
|
package/packages/ui/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/ui",
|
|
3
|
-
"version": "0.13.2-rc.
|
|
3
|
+
"version": "0.13.2-rc.3",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "ai-profile-settings.js",
|
|
6
6
|
"browser": "ai-profile-settings.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@claude-ai-workbench/core": "0.13.2-rc.
|
|
36
|
+
"@claude-ai-workbench/core": "0.13.2-rc.3"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=18"
|