@sanqianx/claude-ai-workbench 0.13.2-rc.2 → 0.13.2-rc.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/README.md +3 -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/cli-json-runner.js +1 -10
- package/packages/server/lib/drivers/codex-driver.js +3 -3
- package/packages/server/lib/drivers/opencode-driver.js +11 -5
- package/packages/server/lib/drivers/zcode-driver.js +66 -19
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -0
- package/packages/server/lib/json-file.js +11 -4
- package/packages/server/lib/workbench-service.js +3 -0
- package/packages/server/package.json +2 -2
- package/packages/ui/package.json +2 -2
- package/src/backend/lib/claude-cli-runner.js +2 -2
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
工作台是产品供应商与凭据的活动配置源。后台维护默认模型持久化,不随会话临时模型切换;解析后的凭据只交给鉴权本地服务,不进入浏览器消息。SDK 会话从当前项目登记的工作区恢复,也兼容旧登记文件。
|
|
14
14
|
|
|
15
|
+
四客户端的会话历史使用同目录临时文件原子替换。磁盘写满、临时写入失败或 Windows 拒绝替换时保留上一次完整记录,不截断或删除原历史;空间恢复后新会话仍可正常持久化。已经被旧版截断的历史不能凭空恢复。
|
|
16
|
+
|
|
15
17
|
MiniMax 向量模型使用独立的 `/v1/embeddings` 接口,不复用聊天的 `/anthropic` 路由。官方域名的已知接口后缀在运行期规范化为服务根地址,现有配置文件不被迁移或改写;自定义网关路径保持原样。
|
|
16
18
|
|
|
17
19
|
Windows 上 Claude Code 探测与实际启动使用同一解析结果:失效的 `CLAUDE_CODE_EXECPATH` 不遮住有效安装,npm/CMD/PowerShell 启动脚本解析成实际文件,JavaScript 入口用当前 Node 探测版本。`/system/doctor` 返回实际启动路径、被跳过的失效路径及版本探测错误。发现和会话失败使用 `log/v2`;受外壳守护时写入外壳统一诊断,独立运行保存在工作台数据目录的 `diagnostics.jsonl`,不记录凭据或原始提示词。
|
|
@@ -62,7 +64,7 @@ npm run start:agent-terminal
|
|
|
62
64
|
- 数据目录:`~/.agent-terminal`(`AGENT_TERMINAL_DATA_DIR` 覆盖;端口 `AGENT_TERMINAL_PORT`)
|
|
63
65
|
- 首次使用:设置 → 模型供应商 → 添加供应商(Base URL / API Key / 模型列表),然后添加本地项目文件夹即可开聊。
|
|
64
66
|
- Agent 发现自动扫描本机:Claude Code(Agent SDK,支持 Anthropic 兼容端点如 MiniMax)、Codex(app-server,保留 CLI 回退)、OpenCode(serve + SSE,保留 CLI 回退)、ZCode(桌面内置 app-server)。版本探测成功不等于认证或任务执行成功,实际运行失败另写诊断日志。
|
|
65
|
-
- ZCode
|
|
67
|
+
- ZCode 原生认证使用已有桌面供应商配置,保留显式环境变量覆盖及个人配置;共享 Workbench 模型通过临时本机连接接入,上游 API Key 只在内存中,临时文件仅保存本机访问令牌,关闭时清理。不改写桌面安装资源或用户模型配置。同时支持旧版 runtimeModel 与新版 modelSelection 协议,失败和完成事件均结束当前任务。
|
|
66
68
|
- 零点击模型检测:填好 Base URL 与 API Key 后自动探测 `/v1/models` 并合并 models.dev 能力数据(上下文窗口、模态、推理),保存时自动填充模型列表与上下文大小。
|
|
67
69
|
- 会话内模型切换:切换供应商/模型不换会话,历史跨供应商延续(Claude 保留 resume,CLI 保留线程参数)。
|
|
68
70
|
- 运行时体验对齐:中断不再误报退出码错误、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.4",
|
|
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.4"
|
|
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.4",
|
|
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.4"
|
|
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 };
|
|
@@ -292,16 +292,7 @@ function createCliJsonRunner(config) {
|
|
|
292
292
|
try {
|
|
293
293
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
294
294
|
fs.writeFileSync(tmp, payload, 'utf8');
|
|
295
|
-
|
|
296
|
-
fs.renameSync(tmp, file); // same-volume atomic replace
|
|
297
|
-
} catch {
|
|
298
|
-
// Windows can refuse to replace a destination an AV scanner or
|
|
299
|
-
// indexer still holds open: drop the old copy and retry the move.
|
|
300
|
-
// The temp file always carries the complete new version, so the
|
|
301
|
-
// destination is only ever missing for one rename call.
|
|
302
|
-
try { fs.unlinkSync(file); } catch { /* nothing to drop */ }
|
|
303
|
-
fs.renameSync(tmp, file);
|
|
304
|
-
}
|
|
295
|
+
fs.renameSync(tmp, file); // failed replacement keeps the old history
|
|
305
296
|
persistFailures = 0;
|
|
306
297
|
if (diskIndex) diskIndex.set(session.sessionId, { file, summary: summary(session) });
|
|
307
298
|
return;
|
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
+
const { writeJson } = require('../json-file');
|
|
7
8
|
const { createCliJsonRunner, resolveCliInvocation } = require('./cli-json-runner');
|
|
8
9
|
const { createAppServerBridge } = require('../bridges/app-server-bridge');
|
|
9
10
|
const {
|
|
@@ -335,8 +336,7 @@ function createAppServerCodexDriver(options = {}) {
|
|
|
335
336
|
if (!appServerPersist.enabled) return;
|
|
336
337
|
try {
|
|
337
338
|
const file = appServerRecordFile(session.sessionId);
|
|
338
|
-
|
|
339
|
-
fs.writeFileSync(file, `${JSON.stringify({
|
|
339
|
+
writeJson(file, {
|
|
340
340
|
schema: APP_SERVER_PERSIST_SCHEMA,
|
|
341
341
|
agentId: 'codex',
|
|
342
342
|
transport: 'app-server',
|
|
@@ -360,7 +360,7 @@ function createAppServerCodexDriver(options = {}) {
|
|
|
360
360
|
peakTurnTokens: session.peakTurnTokens || 0,
|
|
361
361
|
events: session.outputBuffer.slice(-MAX_APP_SERVER_PERSISTED_EVENTS),
|
|
362
362
|
updatedAt: new Date().toISOString(),
|
|
363
|
-
})
|
|
363
|
+
});
|
|
364
364
|
if (appServerDiskIndex) appServerDiskIndex.set(session.sessionId, { file: appServerRecordFile(session.sessionId), summary: summary(session) });
|
|
365
365
|
} catch {
|
|
366
366
|
// Read-only home or full disk degrades to memory-only behavior.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const { writeJson } = require('../json-file');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const crypto = require('crypto');
|
|
@@ -249,8 +250,7 @@ function createServeOpenCodeDriver(options = {}) {
|
|
|
249
250
|
if (!persist.enabled) return;
|
|
250
251
|
try {
|
|
251
252
|
const file = serveRecordFile(session.sessionId);
|
|
252
|
-
|
|
253
|
-
fs.writeFileSync(file, `${JSON.stringify({
|
|
253
|
+
writeJson(file, {
|
|
254
254
|
schema: SERVE_PERSIST_SCHEMA,
|
|
255
255
|
agentId: 'opencode',
|
|
256
256
|
transport: 'serve',
|
|
@@ -275,7 +275,7 @@ function createServeOpenCodeDriver(options = {}) {
|
|
|
275
275
|
peakTurnTokens: session.peakTurnTokens || 0,
|
|
276
276
|
events: session.outputBuffer.slice(-MAX_SERVE_PERSISTED_EVENTS),
|
|
277
277
|
updatedAt: new Date().toISOString(),
|
|
278
|
-
})
|
|
278
|
+
});
|
|
279
279
|
if (serveDiskIndex) serveDiskIndex.set(session.sessionId, { file: serveRecordFile(session.sessionId), summary: summary(session) });
|
|
280
280
|
} catch {
|
|
281
281
|
// Read-only home or full disk degrades to memory-only behavior.
|
|
@@ -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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const { writeJson } = require('../json-file');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const crypto = require('crypto');
|
|
@@ -234,6 +235,11 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
234
235
|
let nextId = 1;
|
|
235
236
|
let nextPermissionId = 1;
|
|
236
237
|
let bridge = null;
|
|
238
|
+
let protocolMode = null;
|
|
239
|
+
let profileRuntime = null;
|
|
240
|
+
let profileRuntimeSignature = null;
|
|
241
|
+
let bridgeSetup = Promise.resolve();
|
|
242
|
+
let destroyed = false;
|
|
237
243
|
let desktopProviderCache; // lazily-read ~/.zcode/v2 providers (agent-owned auth)
|
|
238
244
|
|
|
239
245
|
function summary(session) {
|
|
@@ -317,8 +323,7 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
317
323
|
if (!persist.enabled) return;
|
|
318
324
|
try {
|
|
319
325
|
const file = recordFile(session.sessionId);
|
|
320
|
-
|
|
321
|
-
fs.writeFileSync(file, `${JSON.stringify({
|
|
326
|
+
writeJson(file, {
|
|
322
327
|
schema: APP_SERVER_PERSIST_SCHEMA,
|
|
323
328
|
agentId: 'zcode',
|
|
324
329
|
transport: 'app-server',
|
|
@@ -341,7 +346,7 @@ function createAppServerZCodeDriver(options, cli) {
|
|
|
341
346
|
peakTurnTokens: session.peakTurnTokens || 0,
|
|
342
347
|
events: session.outputBuffer.slice(-MAX_PERSISTED_EVENTS),
|
|
343
348
|
updatedAt: new Date().toISOString(),
|
|
344
|
-
})
|
|
349
|
+
});
|
|
345
350
|
if (diskIndex) diskIndex.set(session.sessionId, { file: recordFile(session.sessionId), summary: summary(session) });
|
|
346
351
|
} catch {
|
|
347
352
|
// Read-only home or full disk degrades to memory-only behavior.
|
|
@@ -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 };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
|
|
6
7
|
function readJson(file, fallback) {
|
|
7
8
|
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
@@ -10,10 +11,16 @@ function readJson(file, fallback) {
|
|
|
10
11
|
|
|
11
12
|
function writeJson(file, value, mode = 0o600) {
|
|
12
13
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
13
|
-
const temp = `${file}.${process.pid}.tmp`;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const temp = `${file}.${process.pid}-${crypto.randomUUID()}.tmp`;
|
|
15
|
+
try {
|
|
16
|
+
fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode, flag: 'wx' });
|
|
17
|
+
// Never unlink the previous record if Windows refuses replacement.
|
|
18
|
+
// ENOSPC or a locked destination must preserve the last durable history.
|
|
19
|
+
fs.renameSync(temp, file);
|
|
20
|
+
try { fs.chmodSync(file, mode); } catch {}
|
|
21
|
+
} finally {
|
|
22
|
+
try { fs.unlinkSync(temp); } catch {}
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
module.exports = { readJson, writeJson };
|
|
@@ -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.4",
|
|
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.4"
|
|
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.4",
|
|
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.4"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=18"
|
|
@@ -19,6 +19,7 @@ const { execFileSync, spawn } = require('child_process');
|
|
|
19
19
|
const crypto = require('crypto');
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
const path = require('path');
|
|
22
|
+
const { writeJson } = require('../../../packages/server/lib/json-file');
|
|
22
23
|
const { renderPrompt } = require('./prompt-registry');
|
|
23
24
|
const aiWorkspace = require('./ai-workspace');
|
|
24
25
|
const {
|
|
@@ -223,8 +224,7 @@ function persistSession(session) {
|
|
|
223
224
|
const file = sessionRecordPath(session);
|
|
224
225
|
if (!file) return;
|
|
225
226
|
try {
|
|
226
|
-
|
|
227
|
-
fs.writeFileSync(file, JSON.stringify(toPersistedSession(session), null, 2) + '\n', 'utf-8');
|
|
227
|
+
writeJson(file, toPersistedSession(session));
|
|
228
228
|
} catch (e) {
|
|
229
229
|
// Persistence should never interrupt the live Claude process.
|
|
230
230
|
}
|