shennian 0.2.6 → 0.2.7
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/dist/bin/shennian.d.ts +2 -0
- package/dist/bin/shennian.js +2 -0
- package/dist/src/agents/adapter.d.ts +35 -0
- package/dist/src/agents/adapter.js +19 -0
- package/dist/src/agents/claude.d.ts +25 -0
- package/dist/src/agents/claude.js +234 -0
- package/dist/src/agents/codex.d.ts +52 -0
- package/dist/src/agents/codex.js +829 -0
- package/dist/src/agents/command-spec.d.ts +36 -0
- package/dist/src/agents/command-spec.js +303 -0
- package/dist/src/agents/cursor.d.ts +22 -0
- package/dist/src/agents/cursor.js +248 -0
- package/dist/src/agents/custom.d.ts +30 -0
- package/dist/src/agents/custom.js +210 -0
- package/dist/src/agents/detect.d.ts +9 -0
- package/dist/src/agents/detect.js +46 -0
- package/dist/src/agents/gemini.d.ts +17 -0
- package/dist/src/agents/gemini.js +172 -0
- package/dist/src/agents/model-registry/cache.d.ts +4 -0
- package/dist/src/agents/model-registry/cache.js +37 -0
- package/dist/src/agents/model-registry/discovery.d.ts +4 -0
- package/dist/src/agents/model-registry/discovery.js +166 -0
- package/dist/src/agents/model-registry/parsers.d.ts +11 -0
- package/dist/src/agents/model-registry/parsers.js +349 -0
- package/dist/src/agents/model-registry/runner.d.ts +6 -0
- package/dist/src/agents/model-registry/runner.js +29 -0
- package/dist/src/agents/model-registry/service.d.ts +6 -0
- package/dist/src/agents/model-registry/service.js +71 -0
- package/dist/src/agents/model-registry/types.d.ts +30 -0
- package/dist/src/agents/model-registry/types.js +8 -0
- package/dist/src/agents/model-registry.d.ts +13 -0
- package/dist/src/agents/model-registry.js +15 -0
- package/dist/src/agents/openclaw.d.ts +20 -0
- package/dist/src/agents/openclaw.js +273 -0
- package/dist/src/agents/pi.d.ts +51 -0
- package/dist/src/agents/pi.js +793 -0
- package/dist/src/commands/agent.d.ts +2 -0
- package/dist/src/commands/agent.js +131 -0
- package/dist/src/commands/daemon.d.ts +35 -0
- package/dist/src/commands/daemon.js +612 -0
- package/dist/src/commands/pair-qr.d.ts +4 -0
- package/dist/src/commands/pair-qr.js +6 -0
- package/dist/src/commands/pair.d.ts +8 -0
- package/dist/src/commands/pair.js +162 -0
- package/dist/src/commands/upgrade.d.ts +5 -0
- package/dist/src/commands/upgrade.js +145 -0
- package/dist/src/config/index.d.ts +32 -0
- package/dist/src/config/index.js +31 -0
- package/dist/src/fs/handler.d.ts +26 -0
- package/dist/src/fs/handler.js +130 -0
- package/dist/src/fs/security.d.ts +2 -0
- package/dist/src/fs/security.js +32 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +277 -0
- package/dist/src/log-reporter.d.ts +18 -0
- package/dist/src/log-reporter.js +16 -0
- package/dist/src/native-fusion/parsers.d.ts +12 -0
- package/dist/src/native-fusion/parsers.js +687 -0
- package/dist/src/native-fusion/service.d.ts +25 -0
- package/dist/src/native-fusion/service.js +176 -0
- package/dist/src/native-fusion/state.d.ts +3 -0
- package/dist/src/native-fusion/state.js +22 -0
- package/dist/src/native-fusion/types.d.ts +24 -0
- package/dist/src/native-fusion/types.js +1 -0
- package/dist/src/region.d.ts +15 -0
- package/dist/src/region.js +99 -0
- package/dist/src/relay/client.d.ts +62 -0
- package/dist/src/relay/client.js +311 -0
- package/dist/src/session/handlers/agents.d.ts +4 -0
- package/dist/src/session/handlers/agents.js +55 -0
- package/dist/src/session/handlers/chat.d.ts +4 -0
- package/dist/src/session/handlers/chat.js +326 -0
- package/dist/src/session/handlers/control.d.ts +5 -0
- package/dist/src/session/handlers/control.js +57 -0
- package/dist/src/session/handlers/fs.d.ts +10 -0
- package/dist/src/session/handlers/fs.js +256 -0
- package/dist/src/session/manager.d.ts +28 -0
- package/dist/src/session/manager.js +174 -0
- package/dist/src/session/store.d.ts +7 -0
- package/dist/src/session/store.js +46 -0
- package/dist/src/session/types.d.ts +35 -0
- package/dist/src/session/types.js +4 -0
- package/dist/src/upgrade/engine.d.ts +64 -0
- package/dist/src/upgrade/engine.js +238 -0
- package/package.json +2 -2
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReqFrame } from '@shennian/wire';
|
|
2
|
+
import type { SessionManagerRuntime } from '../types.js';
|
|
3
|
+
export declare function handleRegionProbe(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
4
|
+
export declare function handleRegionSwitch(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
5
|
+
export declare function handleUpgradeSetPolicy(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/daemon.md#会话管理
|
|
2
|
+
// @test src/__tests__/model-switching.test.ts
|
|
3
|
+
import { SERVERS } from '../../region.js';
|
|
4
|
+
import { loadConfig, saveConfig } from '../../config/index.js';
|
|
5
|
+
export async function handleRegionProbe(runtime, req) {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
const currentUrl = config.serverUrl ?? SERVERS.global.url;
|
|
8
|
+
const currentRegion = currentUrl.includes('shennian.net') ? 'cn' : 'global';
|
|
9
|
+
const results = await Promise.all(Object.keys(SERVERS).map(async (region) => {
|
|
10
|
+
const { url, label } = SERVERS[region];
|
|
11
|
+
const start = performance.now();
|
|
12
|
+
try {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
15
|
+
await fetch(`${url}/health`, { signal: controller.signal });
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
return { region, url, label, latencyMs: Math.round(performance.now() - start) };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { region, url, label, latencyMs: null };
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
runtime.client.sendRes({
|
|
24
|
+
type: 'res',
|
|
25
|
+
id: req.id,
|
|
26
|
+
ok: true,
|
|
27
|
+
payload: { current: currentRegion, servers: results },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function handleRegionSwitch(runtime, req) {
|
|
31
|
+
const targetRegion = req.params.region;
|
|
32
|
+
if (targetRegion !== 'cn' && targetRegion !== 'global') {
|
|
33
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'region must be "cn" or "global"' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
config.serverUrl = SERVERS[targetRegion].url;
|
|
38
|
+
saveConfig(config);
|
|
39
|
+
runtime.client.sendRes({
|
|
40
|
+
type: 'res',
|
|
41
|
+
id: req.id,
|
|
42
|
+
ok: true,
|
|
43
|
+
payload: { region: targetRegion, url: SERVERS[targetRegion].url },
|
|
44
|
+
});
|
|
45
|
+
setTimeout(() => process.exit(0), 500);
|
|
46
|
+
}
|
|
47
|
+
export async function handleUpgradeSetPolicy(runtime, req) {
|
|
48
|
+
const policy = req.params.policy;
|
|
49
|
+
if (policy !== 'none' && policy !== 'patch' && policy !== 'minor') {
|
|
50
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'policy must be "none", "patch", or "minor"' });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
config.autoUpgrade = policy;
|
|
55
|
+
saveConfig(config);
|
|
56
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { autoUpgrade: policy } });
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReqFrame } from '@shennian/wire';
|
|
2
|
+
import type { SessionManagerRuntime } from '../types.js';
|
|
3
|
+
export declare function handleFsLs(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
4
|
+
export declare function handleFsRead(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
5
|
+
export declare function handleFsTransfer(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
6
|
+
export declare function handleFsTransferStart(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
7
|
+
export declare function handleFsTransferChunk(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
8
|
+
export declare function handleFsTransferFinish(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
9
|
+
export declare function handleFsTransferAbort(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
10
|
+
export declare function cleanupPendingTransfers(runtime: SessionManagerRuntime): void;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/daemon.md#会话管理
|
|
2
|
+
// @test src/__tests__/session-manager.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
const FILE_SYSTEM_ROOTS_PATH = '__roots__';
|
|
7
|
+
function isWindowsAbsolutePath(pathValue) {
|
|
8
|
+
return /^[A-Za-z]:([\\/]|$)/.test(pathValue) || /^\\\\[^\\]+\\[^\\]+/.test(pathValue);
|
|
9
|
+
}
|
|
10
|
+
function listFileSystemRoots() {
|
|
11
|
+
if (os.platform() === 'win32') {
|
|
12
|
+
const entries = [];
|
|
13
|
+
for (let code = 65; code <= 90; code += 1) {
|
|
14
|
+
const drive = `${String.fromCharCode(code)}:\\`;
|
|
15
|
+
if (!fs.existsSync(drive))
|
|
16
|
+
continue;
|
|
17
|
+
entries.push({
|
|
18
|
+
name: drive,
|
|
19
|
+
path: drive,
|
|
20
|
+
isDir: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return { path: FILE_SYSTEM_ROOTS_PATH, entries };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
path: FILE_SYSTEM_ROOTS_PATH,
|
|
27
|
+
entries: [{ name: '/', path: '/', isDir: true }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function handleFsLs(runtime, req) {
|
|
31
|
+
const requestedPath = req.params.path || os.homedir();
|
|
32
|
+
if (requestedPath === FILE_SYSTEM_ROOTS_PATH) {
|
|
33
|
+
runtime.client.sendRes({
|
|
34
|
+
type: 'res',
|
|
35
|
+
id: req.id,
|
|
36
|
+
ok: true,
|
|
37
|
+
payload: listFileSystemRoots(),
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const dirPath = runtime.resolvePath(requestedPath);
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
44
|
+
const joinPath = isWindowsAbsolutePath(dirPath) ? path.win32.join : path.join;
|
|
45
|
+
const entries = raw
|
|
46
|
+
.map((entry) => ({
|
|
47
|
+
name: entry.name,
|
|
48
|
+
path: joinPath(dirPath, entry.name),
|
|
49
|
+
isDir: entry.isDirectory(),
|
|
50
|
+
}))
|
|
51
|
+
.sort((left, right) => {
|
|
52
|
+
if (left.isDir !== right.isDir)
|
|
53
|
+
return left.isDir ? -1 : 1;
|
|
54
|
+
return left.name.localeCompare(right.name);
|
|
55
|
+
});
|
|
56
|
+
runtime.client.sendRes({
|
|
57
|
+
type: 'res',
|
|
58
|
+
id: req.id,
|
|
59
|
+
ok: true,
|
|
60
|
+
payload: { path: dirPath, entries },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function handleFsRead(runtime, req) {
|
|
68
|
+
const filePath = runtime.resolvePath(req.params.path);
|
|
69
|
+
const encoding = req.params.encoding || 'utf8';
|
|
70
|
+
const offset = req.params.offset;
|
|
71
|
+
const length = req.params.length;
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.statSync(filePath);
|
|
74
|
+
if (!stat.isFile()) {
|
|
75
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Not a file' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (offset != null && length != null) {
|
|
79
|
+
const readLen = Math.min(length, Math.max(0, stat.size - offset));
|
|
80
|
+
if (readLen <= 0) {
|
|
81
|
+
runtime.client.sendRes({
|
|
82
|
+
type: 'res',
|
|
83
|
+
id: req.id,
|
|
84
|
+
ok: true,
|
|
85
|
+
payload: { data: '', offset, length: 0, totalSize: stat.size, path: filePath },
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const fd = fs.openSync(filePath, 'r');
|
|
90
|
+
try {
|
|
91
|
+
const buffer = Buffer.alloc(readLen);
|
|
92
|
+
const bytesRead = fs.readSync(fd, buffer, 0, readLen, offset);
|
|
93
|
+
runtime.client.sendRes({
|
|
94
|
+
type: 'res',
|
|
95
|
+
id: req.id,
|
|
96
|
+
ok: true,
|
|
97
|
+
payload: {
|
|
98
|
+
data: buffer.subarray(0, bytesRead).toString('base64'),
|
|
99
|
+
offset,
|
|
100
|
+
length: bytesRead,
|
|
101
|
+
totalSize: stat.size,
|
|
102
|
+
path: filePath,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
fs.closeSync(fd);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const maxSize = req.params.maxSize || (encoding === 'base64' ? 5 * 1024 * 1024 : 512 * 1024);
|
|
112
|
+
if (stat.size > maxSize) {
|
|
113
|
+
runtime.client.sendRes({
|
|
114
|
+
type: 'res',
|
|
115
|
+
id: req.id,
|
|
116
|
+
ok: false,
|
|
117
|
+
error: `File too large: ${stat.size} bytes (max ${maxSize})`,
|
|
118
|
+
payload: { size: stat.size, path: filePath },
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (encoding === 'base64') {
|
|
123
|
+
const buffer = fs.readFileSync(filePath);
|
|
124
|
+
runtime.client.sendRes({
|
|
125
|
+
type: 'res',
|
|
126
|
+
id: req.id,
|
|
127
|
+
ok: true,
|
|
128
|
+
payload: { data: buffer.toString('base64'), path: filePath, size: stat.size },
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
133
|
+
runtime.client.sendRes({
|
|
134
|
+
type: 'res',
|
|
135
|
+
id: req.id,
|
|
136
|
+
ok: true,
|
|
137
|
+
payload: { content, path: filePath, size: stat.size },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function handleFsTransfer(runtime, req) {
|
|
145
|
+
const { name, targetPath, data, direct } = req.params;
|
|
146
|
+
if (!name || !data) {
|
|
147
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'name and data are required' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const baseDir = runtime.resolvePath(targetPath || os.homedir());
|
|
152
|
+
const uploadDir = direct ? baseDir : path.join(baseDir, '.uploads');
|
|
153
|
+
if (!direct)
|
|
154
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
155
|
+
const filePath = path.join(uploadDir, path.basename(name));
|
|
156
|
+
const buffer = Buffer.from(data, 'base64');
|
|
157
|
+
fs.writeFileSync(filePath, buffer);
|
|
158
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { path: filePath } });
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export async function handleFsTransferStart(runtime, req) {
|
|
165
|
+
const { name, targetPath, totalSize, direct } = req.params;
|
|
166
|
+
if (!name || !totalSize) {
|
|
167
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'name and totalSize are required' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const baseDir = runtime.resolvePath(targetPath || os.homedir());
|
|
172
|
+
const destinationDir = direct ? baseDir : path.join(baseDir, '.uploads');
|
|
173
|
+
if (!direct)
|
|
174
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
175
|
+
const transferId = `tf-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
176
|
+
const tempPath = path.join(os.tmpdir(), `.shennian-upload-${transferId}`);
|
|
177
|
+
const finalPath = path.join(destinationDir, path.basename(name));
|
|
178
|
+
fs.writeFileSync(tempPath, Buffer.alloc(0));
|
|
179
|
+
runtime.pendingTransfers.set(transferId, { tempPath, targetPath: finalPath, totalSize });
|
|
180
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { transferId } });
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export async function handleFsTransferChunk(runtime, req) {
|
|
187
|
+
const { transferId, offset, data } = req.params;
|
|
188
|
+
const transfer = runtime.pendingTransfers.get(transferId);
|
|
189
|
+
if (!transfer) {
|
|
190
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid transferId' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const buffer = Buffer.from(data, 'base64');
|
|
195
|
+
const fd = fs.openSync(transfer.tempPath, 'r+');
|
|
196
|
+
try {
|
|
197
|
+
fs.writeSync(fd, buffer, 0, buffer.length, offset);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
fs.closeSync(fd);
|
|
201
|
+
}
|
|
202
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { written: buffer.length } });
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export async function handleFsTransferFinish(runtime, req) {
|
|
209
|
+
const { transferId } = req.params;
|
|
210
|
+
const transfer = runtime.pendingTransfers.get(transferId);
|
|
211
|
+
if (!transfer) {
|
|
212
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid transferId' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
fs.renameSync(transfer.tempPath, transfer.targetPath);
|
|
217
|
+
runtime.pendingTransfers.delete(transferId);
|
|
218
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { path: transfer.targetPath } });
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
try {
|
|
222
|
+
fs.copyFileSync(transfer.tempPath, transfer.targetPath);
|
|
223
|
+
fs.unlinkSync(transfer.tempPath);
|
|
224
|
+
runtime.pendingTransfers.delete(transferId);
|
|
225
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { path: transfer.targetPath } });
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export async function handleFsTransferAbort(runtime, req) {
|
|
233
|
+
const { transferId } = req.params;
|
|
234
|
+
const transfer = runtime.pendingTransfers.get(transferId);
|
|
235
|
+
if (transfer) {
|
|
236
|
+
try {
|
|
237
|
+
fs.unlinkSync(transfer.tempPath);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// ignore cleanup failures
|
|
241
|
+
}
|
|
242
|
+
runtime.pendingTransfers.delete(transferId);
|
|
243
|
+
}
|
|
244
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
245
|
+
}
|
|
246
|
+
export function cleanupPendingTransfers(runtime) {
|
|
247
|
+
for (const [, transfer] of runtime.pendingTransfers) {
|
|
248
|
+
try {
|
|
249
|
+
fs.unlinkSync(transfer.tempPath);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// ignore cleanup failures
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
runtime.pendingTransfers.clear();
|
|
256
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReqFrame } from '@shennian/wire';
|
|
2
|
+
import type { CliRelayClient } from '../relay/client.js';
|
|
3
|
+
import type { NativeSessionFusionService } from '../native-fusion/service.js';
|
|
4
|
+
import '../agents/claude.js';
|
|
5
|
+
import '../agents/codex.js';
|
|
6
|
+
import '../agents/gemini.js';
|
|
7
|
+
import '../agents/cursor.js';
|
|
8
|
+
import '../agents/openclaw.js';
|
|
9
|
+
import '../agents/pi.js';
|
|
10
|
+
export declare function resolveSessionWorkDir(input: string): string;
|
|
11
|
+
export declare class SessionManager {
|
|
12
|
+
private client;
|
|
13
|
+
private nativeFusion;
|
|
14
|
+
private sessions;
|
|
15
|
+
/** Track processed request IDs to deduplicate replayed offline messages */
|
|
16
|
+
private processedReqIds;
|
|
17
|
+
/** Accumulate agent text per run for messageSummary on final event */
|
|
18
|
+
private runTextAcc;
|
|
19
|
+
/** In-flight chunked uploads: transferId → metadata */
|
|
20
|
+
private pendingTransfers;
|
|
21
|
+
constructor(client: CliRelayClient, nativeFusion?: NativeSessionFusionService | null);
|
|
22
|
+
private getRuntime;
|
|
23
|
+
private reloadCustomAgents;
|
|
24
|
+
handleReq(req: ReqFrame): Promise<void>;
|
|
25
|
+
private evictIdleSessions;
|
|
26
|
+
private resolvePath;
|
|
27
|
+
cleanup(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/daemon.md#会话管理
|
|
2
|
+
// @test src/__tests__/session-store.test.ts
|
|
3
|
+
// @test src/__tests__/model-switching.test.ts
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { getRegisteredAgents, unregisterAgent } from '../agents/adapter.js';
|
|
7
|
+
import { loadConfig } from '../config/index.js';
|
|
8
|
+
import { handleUpgradeStart, handleUpgradeStatus } from '../commands/upgrade.js';
|
|
9
|
+
import { handleAgentsRefresh, handleModelsRefresh } from './handlers/agents.js';
|
|
10
|
+
import { handleChatAbort, handleChatSend } from './handlers/chat.js';
|
|
11
|
+
import { cleanupPendingTransfers, handleFsLs, handleFsRead, handleFsTransfer, handleFsTransferAbort, handleFsTransferChunk, handleFsTransferFinish, handleFsTransferStart, } from './handlers/fs.js';
|
|
12
|
+
import { handleRegionProbe, handleRegionSwitch, handleUpgradeSetPolicy } from './handlers/control.js';
|
|
13
|
+
// Side-effect imports to register built-in agent adapters.
|
|
14
|
+
import '../agents/claude.js';
|
|
15
|
+
import '../agents/codex.js';
|
|
16
|
+
import '../agents/gemini.js';
|
|
17
|
+
import '../agents/cursor.js';
|
|
18
|
+
import '../agents/openclaw.js';
|
|
19
|
+
import '../agents/pi.js';
|
|
20
|
+
import { registerCustomAgent } from '../agents/custom.js';
|
|
21
|
+
const MAX_SESSIONS = 50;
|
|
22
|
+
function isWindowsAbsolutePath(input) {
|
|
23
|
+
return /^[A-Za-z]:([\\/]|$)/.test(input) || /^\\\\[^\\]+\\[^\\]+/.test(input);
|
|
24
|
+
}
|
|
25
|
+
export function resolveSessionWorkDir(input) {
|
|
26
|
+
if (!input)
|
|
27
|
+
return os.homedir();
|
|
28
|
+
const joinPath = process.platform === 'win32' ? path.win32.join : path.join;
|
|
29
|
+
if (input.startsWith('~')) {
|
|
30
|
+
return joinPath(os.homedir(), input.slice(1).replace(/^[/\\]+/, ''));
|
|
31
|
+
}
|
|
32
|
+
if (process.platform === 'win32') {
|
|
33
|
+
const normalized = input.replace(/\\/g, '/');
|
|
34
|
+
if (normalized === '/tmp' || normalized.startsWith('/tmp/')) {
|
|
35
|
+
const suffix = normalized.slice('/tmp'.length).replace(/^\/+/, '');
|
|
36
|
+
return suffix ? path.win32.join(os.tmpdir(), ...suffix.split('/')) : os.tmpdir();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (isWindowsAbsolutePath(input)) {
|
|
40
|
+
return path.win32.resolve(input);
|
|
41
|
+
}
|
|
42
|
+
return path.resolve(input);
|
|
43
|
+
}
|
|
44
|
+
export class SessionManager {
|
|
45
|
+
client;
|
|
46
|
+
nativeFusion;
|
|
47
|
+
sessions = new Map();
|
|
48
|
+
/** Track processed request IDs to deduplicate replayed offline messages */
|
|
49
|
+
processedReqIds = new Set();
|
|
50
|
+
/** Accumulate agent text per run for messageSummary on final event */
|
|
51
|
+
runTextAcc = new Map();
|
|
52
|
+
/** In-flight chunked uploads: transferId → metadata */
|
|
53
|
+
pendingTransfers = new Map();
|
|
54
|
+
constructor(client, nativeFusion = null) {
|
|
55
|
+
this.client = client;
|
|
56
|
+
this.nativeFusion = nativeFusion;
|
|
57
|
+
this.reloadCustomAgents();
|
|
58
|
+
}
|
|
59
|
+
getRuntime() {
|
|
60
|
+
return {
|
|
61
|
+
client: this.client,
|
|
62
|
+
pendingTransfers: this.pendingTransfers,
|
|
63
|
+
processedReqIds: this.processedReqIds,
|
|
64
|
+
reloadCustomAgents: () => this.reloadCustomAgents(),
|
|
65
|
+
resolvePath: (pathValue) => this.resolvePath(pathValue),
|
|
66
|
+
runTextAcc: this.runTextAcc,
|
|
67
|
+
sessions: this.sessions,
|
|
68
|
+
evictIdleSessions: () => this.evictIdleSessions(),
|
|
69
|
+
nativeFusion: this.nativeFusion,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
reloadCustomAgents() {
|
|
73
|
+
for (const agentType of getRegisteredAgents()) {
|
|
74
|
+
if (agentType.startsWith('custom:')) {
|
|
75
|
+
unregisterAgent(agentType);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
for (const [name, entry] of Object.entries(config.customAgents ?? {})) {
|
|
80
|
+
registerCustomAgent(name, entry);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async handleReq(req) {
|
|
84
|
+
const runtime = this.getRuntime();
|
|
85
|
+
try {
|
|
86
|
+
switch (req.method) {
|
|
87
|
+
case 'chat.send':
|
|
88
|
+
await handleChatSend(runtime, req);
|
|
89
|
+
break;
|
|
90
|
+
case 'chat.abort':
|
|
91
|
+
await handleChatAbort(runtime, req);
|
|
92
|
+
break;
|
|
93
|
+
case 'fs.ls':
|
|
94
|
+
await handleFsLs(runtime, req);
|
|
95
|
+
break;
|
|
96
|
+
case 'fs.read':
|
|
97
|
+
await handleFsRead(runtime, req);
|
|
98
|
+
break;
|
|
99
|
+
case 'fs.transfer':
|
|
100
|
+
await handleFsTransfer(runtime, req);
|
|
101
|
+
break;
|
|
102
|
+
case 'fs.transfer.start':
|
|
103
|
+
await handleFsTransferStart(runtime, req);
|
|
104
|
+
break;
|
|
105
|
+
case 'fs.transfer.chunk':
|
|
106
|
+
await handleFsTransferChunk(runtime, req);
|
|
107
|
+
break;
|
|
108
|
+
case 'fs.transfer.finish':
|
|
109
|
+
await handleFsTransferFinish(runtime, req);
|
|
110
|
+
break;
|
|
111
|
+
case 'fs.transfer.abort':
|
|
112
|
+
await handleFsTransferAbort(runtime, req);
|
|
113
|
+
break;
|
|
114
|
+
case 'region.probe':
|
|
115
|
+
await handleRegionProbe(runtime, req);
|
|
116
|
+
break;
|
|
117
|
+
case 'region.switch':
|
|
118
|
+
await handleRegionSwitch(runtime, req);
|
|
119
|
+
break;
|
|
120
|
+
case 'upgrade.start':
|
|
121
|
+
await handleUpgradeStart(this.client, req.id, req.params.version);
|
|
122
|
+
break;
|
|
123
|
+
case 'upgrade.status':
|
|
124
|
+
await handleUpgradeStatus(this.client, req.id);
|
|
125
|
+
break;
|
|
126
|
+
case 'upgrade.set-policy':
|
|
127
|
+
await handleUpgradeSetPolicy(runtime, req);
|
|
128
|
+
break;
|
|
129
|
+
case 'agents.refresh':
|
|
130
|
+
await handleAgentsRefresh(runtime, req);
|
|
131
|
+
break;
|
|
132
|
+
case 'models.refresh':
|
|
133
|
+
await handleModelsRefresh(runtime, req);
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
this.client.sendRes({ type: 'res', id: req.id, ok: false, error: `Unknown method: ${req.method}` });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
this.client.sendRes({
|
|
141
|
+
type: 'res', id: req.id, ok: false,
|
|
142
|
+
error: err instanceof Error ? err.message : String(err),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ─── Session eviction ──────────────────────────────────────────────────────
|
|
147
|
+
evictIdleSessions() {
|
|
148
|
+
if (this.sessions.size < MAX_SESSIONS)
|
|
149
|
+
return;
|
|
150
|
+
let oldest = null;
|
|
151
|
+
for (const [key, session] of this.sessions) {
|
|
152
|
+
if (!oldest || session.lastActiveAt < oldest.session.lastActiveAt) {
|
|
153
|
+
oldest = { key, session };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (oldest) {
|
|
157
|
+
oldest.session.adapter.removeAllListeners();
|
|
158
|
+
oldest.session.adapter.stop().catch(() => { });
|
|
159
|
+
this.sessions.delete(oldest.key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
163
|
+
resolvePath(p) {
|
|
164
|
+
return resolveSessionWorkDir(p);
|
|
165
|
+
}
|
|
166
|
+
cleanup() {
|
|
167
|
+
for (const [, session] of this.sessions) {
|
|
168
|
+
session.adapter.stop().catch(() => { });
|
|
169
|
+
}
|
|
170
|
+
this.sessions.clear();
|
|
171
|
+
this.runTextAcc.clear();
|
|
172
|
+
cleanupPendingTransfers(this.getRuntime());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Envelope } from '@shennian/wire';
|
|
2
|
+
export declare function appendMessage(sessionId: string, envelope: Envelope): void;
|
|
3
|
+
export declare function readMessages(sessionId: string, opts?: {
|
|
4
|
+
limit?: number;
|
|
5
|
+
before?: number;
|
|
6
|
+
}): Envelope[];
|
|
7
|
+
export declare function listSessions(): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveShennianPath } from '../config/index.js';
|
|
4
|
+
const SESSIONS_DIR = resolveShennianPath('sessions');
|
|
5
|
+
function ensureSessionsDir() {
|
|
6
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
function sessionFile(sessionId) {
|
|
9
|
+
return path.join(SESSIONS_DIR, `${sessionId}.jsonl`);
|
|
10
|
+
}
|
|
11
|
+
export function appendMessage(sessionId, envelope) {
|
|
12
|
+
ensureSessionsDir();
|
|
13
|
+
const line = JSON.stringify(envelope) + '\n';
|
|
14
|
+
fs.appendFileSync(sessionFile(sessionId), line, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
export function readMessages(sessionId, opts) {
|
|
17
|
+
const file = sessionFile(sessionId);
|
|
18
|
+
if (!fs.existsSync(file))
|
|
19
|
+
return [];
|
|
20
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
21
|
+
const lines = content.split('\n').filter((l) => l.trim().length > 0);
|
|
22
|
+
let messages = [];
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
try {
|
|
25
|
+
messages.push(JSON.parse(line));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// skip malformed lines
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
messages.sort((a, b) => b.ts - a.ts);
|
|
32
|
+
if (opts?.before !== undefined) {
|
|
33
|
+
messages = messages.filter((m) => m.ts < opts.before);
|
|
34
|
+
}
|
|
35
|
+
if (opts?.limit !== undefined && opts.limit > 0) {
|
|
36
|
+
messages = messages.slice(0, opts.limit);
|
|
37
|
+
}
|
|
38
|
+
return messages;
|
|
39
|
+
}
|
|
40
|
+
export function listSessions() {
|
|
41
|
+
ensureSessionsDir();
|
|
42
|
+
return fs
|
|
43
|
+
.readdirSync(SESSIONS_DIR)
|
|
44
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
45
|
+
.map((f) => f.replace(/\.jsonl$/, ''));
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentType } from '@shennian/wire';
|
|
2
|
+
import type { AgentAdapter } from '../agents/adapter.js';
|
|
3
|
+
import type { CliRelayClient } from '../relay/client.js';
|
|
4
|
+
import type { NativeSessionFusionService } from '../native-fusion/service.js';
|
|
5
|
+
export type ActiveSession = {
|
|
6
|
+
adapter: AgentAdapter;
|
|
7
|
+
workDir: string;
|
|
8
|
+
agentType: AgentType;
|
|
9
|
+
agentSessionId: string | null;
|
|
10
|
+
lastActiveAt: number;
|
|
11
|
+
currentRunId: string | null;
|
|
12
|
+
nextEventSeq: number;
|
|
13
|
+
pendingTextEvent: {
|
|
14
|
+
runId: string;
|
|
15
|
+
seq: number;
|
|
16
|
+
text: string;
|
|
17
|
+
thinking: boolean;
|
|
18
|
+
} | null;
|
|
19
|
+
};
|
|
20
|
+
export type PendingTransfer = {
|
|
21
|
+
tempPath: string;
|
|
22
|
+
targetPath: string;
|
|
23
|
+
totalSize: number;
|
|
24
|
+
};
|
|
25
|
+
export type SessionManagerRuntime = {
|
|
26
|
+
client: CliRelayClient;
|
|
27
|
+
pendingTransfers: Map<string, PendingTransfer>;
|
|
28
|
+
processedReqIds: Set<string>;
|
|
29
|
+
reloadCustomAgents: () => void;
|
|
30
|
+
resolvePath: (pathValue: string) => string;
|
|
31
|
+
runTextAcc: Map<string, string>;
|
|
32
|
+
sessions: Map<string, ActiveSession>;
|
|
33
|
+
evictIdleSessions: () => void;
|
|
34
|
+
nativeFusion: NativeSessionFusionService | null;
|
|
35
|
+
};
|