shennian 0.2.89 → 0.2.90
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/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
package/dist/src/agents/pi.js
CHANGED
|
@@ -1,723 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import { SERVERS } from '../region.js';
|
|
16
|
-
import { buildRollingSummary, buildShellCommandSpec, cloneMessages, CONTEXT_TOKEN_THRESHOLD, createPiModel, estimateTokens, getSessionDir, KEEP_RECENT_MESSAGES, LEGACY_SUMMARY_FILENAME, loadAgentsMdInstructions, longestCommonPrefixLength, MESSAGES_FILENAME, messagesToText, PI_DEFAULT_MODEL_ID, requestProxySummary, SNAPSHOT_FILENAME, SYSTEM_PROMPT, SUMMARY_FILENAME, } from './pi-context.js';
|
|
17
|
-
export { buildShellCommandSpec } from './pi-context.js';
|
|
18
|
-
const execFileAsync = promisify(execFile);
|
|
19
|
-
const DASHSCOPE_COMPATIBLE_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1';
|
|
20
|
-
// ── Local tools ───────────────────────────────────────────────────────────────
|
|
21
|
-
function resolvePiToolPath(workDir, filePath) {
|
|
22
|
-
return path.resolve(workDir, filePath);
|
|
23
|
-
}
|
|
24
|
-
async function executePiShellCommand(workDir, command, extraEnv, signal) {
|
|
25
|
-
const spec = buildShellCommandSpec(command);
|
|
26
|
-
const { stdout, stderr } = await execFileAsync(spec.file, spec.args, {
|
|
27
|
-
cwd: workDir,
|
|
28
|
-
env: { ...process.env, ...extraEnv },
|
|
29
|
-
timeout: 30_000,
|
|
30
|
-
signal,
|
|
31
|
-
maxBuffer: 1024 * 1024,
|
|
32
|
-
windowsHide: true,
|
|
33
|
-
});
|
|
34
|
-
return { stdout, stderr, shell: spec.shell };
|
|
35
|
-
}
|
|
36
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
-
function makeTools(workDir, extraEnv = {}) {
|
|
38
|
-
return [
|
|
39
|
-
{
|
|
40
|
-
name: 'read_file',
|
|
41
|
-
label: '读取文件',
|
|
42
|
-
description: 'Read the full content of a file at the given path.',
|
|
43
|
-
parameters: Type.Object({
|
|
44
|
-
path: Type.String({ description: 'Absolute or relative file path' }),
|
|
45
|
-
}),
|
|
46
|
-
async execute(_id, { path: filePath }) {
|
|
47
|
-
let resolved = '';
|
|
48
|
-
try {
|
|
49
|
-
resolved = resolvePiToolPath(workDir, filePath);
|
|
50
|
-
const content = fs.readFileSync(resolved, 'utf-8');
|
|
51
|
-
return {
|
|
52
|
-
content: [{ type: 'text', text: content }],
|
|
53
|
-
details: { path: resolved },
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
return {
|
|
58
|
-
content: [
|
|
59
|
-
{
|
|
60
|
-
type: 'text',
|
|
61
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
details: { path: resolved },
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
name: 'write_file',
|
|
71
|
-
label: '写入文件',
|
|
72
|
-
description: 'Write content to a file, creating it or overwriting if it exists.',
|
|
73
|
-
parameters: Type.Object({
|
|
74
|
-
path: Type.String({ description: 'Absolute or relative file path' }),
|
|
75
|
-
content: Type.String({ description: 'File content to write' }),
|
|
76
|
-
}),
|
|
77
|
-
async execute(_id, { path: filePath, content }) {
|
|
78
|
-
let resolved = '';
|
|
79
|
-
try {
|
|
80
|
-
resolved = resolvePiToolPath(workDir, filePath);
|
|
81
|
-
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
82
|
-
fs.writeFileSync(resolved, content, 'utf-8');
|
|
83
|
-
return {
|
|
84
|
-
content: [
|
|
85
|
-
{ type: 'text', text: `Written ${content.length} bytes to ${resolved}` },
|
|
86
|
-
],
|
|
87
|
-
details: { path: resolved },
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
return {
|
|
92
|
-
content: [
|
|
93
|
-
{
|
|
94
|
-
type: 'text',
|
|
95
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
details: { path: resolved },
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: 'list_directory',
|
|
105
|
-
label: '列出目录',
|
|
106
|
-
description: 'List files and directories at the given path.',
|
|
107
|
-
parameters: Type.Object({
|
|
108
|
-
path: Type.String({ description: 'Directory path to list', default: '.' }),
|
|
109
|
-
}),
|
|
110
|
-
async execute(_id, { path: dirPath }) {
|
|
111
|
-
let resolved = '';
|
|
112
|
-
try {
|
|
113
|
-
resolved = resolvePiToolPath(workDir, dirPath);
|
|
114
|
-
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
115
|
-
const lines = entries.map((e) => `${e.isDirectory() ? 'd' : 'f'} ${e.name}`).join('\n');
|
|
116
|
-
return {
|
|
117
|
-
content: [{ type: 'text', text: lines || '(empty)' }],
|
|
118
|
-
details: { path: resolved },
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
return {
|
|
123
|
-
content: [
|
|
124
|
-
{
|
|
125
|
-
type: 'text',
|
|
126
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
details: { path: resolved },
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: 'bash',
|
|
136
|
-
label: '执行命令',
|
|
137
|
-
description: 'Execute a shell command in the working directory. Uses PowerShell on Windows and bash on macOS/Linux. Timeout: 30s.',
|
|
138
|
-
parameters: Type.Object({
|
|
139
|
-
command: Type.String({ description: 'Shell command to execute' }),
|
|
140
|
-
}),
|
|
141
|
-
async execute(_id, { command }, signal) {
|
|
142
|
-
try {
|
|
143
|
-
const { stdout, stderr, shell } = await executePiShellCommand(workDir, command, extraEnv, signal);
|
|
144
|
-
const output = [stdout, stderr].filter(Boolean).join('\n---stderr---\n');
|
|
145
|
-
return {
|
|
146
|
-
content: [{ type: 'text', text: output || '(no output)' }],
|
|
147
|
-
details: { command, shell },
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
-
return {
|
|
153
|
-
content: [{ type: 'text', text: `Error: ${msg}` }],
|
|
154
|
-
details: { command, shell: buildShellCommandSpec(command).shell },
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
name: 'read_image',
|
|
161
|
-
label: '读取图片',
|
|
162
|
-
description: 'Read an image file and return its content for visual analysis. Supports png, jpg, jpeg, gif, webp. Max 10 MB.',
|
|
163
|
-
parameters: Type.Object({
|
|
164
|
-
path: Type.String({ description: 'Absolute or relative path to the image file' }),
|
|
165
|
-
}),
|
|
166
|
-
async execute(_id, { path: filePath }) {
|
|
167
|
-
let resolved;
|
|
168
|
-
try {
|
|
169
|
-
resolved = resolvePiToolPath(workDir, filePath);
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
return {
|
|
173
|
-
content: [
|
|
174
|
-
{
|
|
175
|
-
type: 'text',
|
|
176
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
details: { path: filePath },
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
const ext = path.extname(resolved).toLowerCase().slice(1);
|
|
183
|
-
const mimeMap = {
|
|
184
|
-
png: 'image/png',
|
|
185
|
-
jpg: 'image/jpeg',
|
|
186
|
-
jpeg: 'image/jpeg',
|
|
187
|
-
gif: 'image/gif',
|
|
188
|
-
webp: 'image/webp',
|
|
189
|
-
};
|
|
190
|
-
const mimeType = mimeMap[ext];
|
|
191
|
-
if (!mimeType) {
|
|
192
|
-
return {
|
|
193
|
-
content: [
|
|
194
|
-
{
|
|
195
|
-
type: 'text',
|
|
196
|
-
text: `Error: unsupported image type ".${ext}". Supported: png, jpg, jpeg, gif, webp`,
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
details: { path: resolved },
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
const stat = fs.statSync(resolved);
|
|
204
|
-
if (stat.size > 10 * 1024 * 1024) {
|
|
205
|
-
return {
|
|
206
|
-
content: [
|
|
207
|
-
{
|
|
208
|
-
type: 'text',
|
|
209
|
-
text: `Error: image too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 10 MB)`,
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
details: { path: resolved },
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
const data = fs.readFileSync(resolved).toString('base64');
|
|
216
|
-
const imageContent = { type: 'image', data, mimeType };
|
|
217
|
-
return { content: [imageContent], details: { path: resolved, mimeType, size: stat.size } };
|
|
218
|
-
}
|
|
219
|
-
catch (err) {
|
|
220
|
-
return {
|
|
221
|
-
content: [
|
|
222
|
-
{
|
|
223
|
-
type: 'text',
|
|
224
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
details: { path: resolved },
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
];
|
|
233
|
-
}
|
|
234
|
-
// ── PiAdapter ─────────────────────────────────────────────────────────────────
|
|
235
|
-
export class PiAdapter extends AgentAdapter {
|
|
236
|
-
type = 'pi';
|
|
237
|
-
agent = null;
|
|
238
|
-
sessionId = null;
|
|
239
|
-
workDir = null;
|
|
240
|
-
seq = 0;
|
|
241
|
-
runId = '';
|
|
242
|
-
emittedLengths = new Map();
|
|
243
|
-
terminalState = 'open';
|
|
244
|
-
authToken = null;
|
|
245
|
-
proxyUrl = null;
|
|
246
|
-
providerConfig;
|
|
247
|
-
sessionDir = null;
|
|
248
|
-
messagesPath = null;
|
|
249
|
-
snapshotPath = null;
|
|
250
|
-
summaryPath = null;
|
|
251
|
-
cachedSummary = null;
|
|
252
|
-
restoredMessages = [];
|
|
253
|
-
compressing = false;
|
|
254
|
-
lastSummaryMsgCount = 0;
|
|
255
|
-
pendingBaseMessages = [];
|
|
256
|
-
finalizePromise = Promise.resolve();
|
|
257
|
-
sendGeneration = 0;
|
|
258
|
-
externalChannel = null;
|
|
259
|
-
shennianSessionId = null;
|
|
260
|
-
extraEnv = {};
|
|
261
|
-
pendingSendStart = null;
|
|
262
|
-
configure(options) {
|
|
263
|
-
this.shennianSessionId = options.sessionId ?? null;
|
|
264
|
-
this.externalChannel = options.externalChannel ?? null;
|
|
265
|
-
this.extraEnv = options.env ?? {};
|
|
266
|
-
}
|
|
267
|
-
async start(sessionId, workDir, _agentSessionId) {
|
|
268
|
-
this.sessionId = sessionId;
|
|
269
|
-
this.workDir = workDir;
|
|
270
|
-
this.seq = 0;
|
|
271
|
-
this.sessionDir = getSessionDir(sessionId);
|
|
272
|
-
this.messagesPath = path.join(this.sessionDir, MESSAGES_FILENAME);
|
|
273
|
-
this.snapshotPath = path.join(this.sessionDir, SNAPSHOT_FILENAME);
|
|
274
|
-
this.summaryPath = path.join(this.sessionDir, SUMMARY_FILENAME);
|
|
275
|
-
this.loadSnapshot();
|
|
276
|
-
this.loadSummary();
|
|
277
|
-
}
|
|
278
|
-
async send(text, modelId) {
|
|
279
|
-
const generation = ++this.sendGeneration;
|
|
280
|
-
const interruptedMessages = this.agent && this.terminalState === 'open'
|
|
281
|
-
? cloneMessages(this.pendingBaseMessages)
|
|
282
|
-
: null;
|
|
283
|
-
if (interruptedMessages) {
|
|
284
|
-
this.agent?.abort();
|
|
285
|
-
this.agent = null;
|
|
286
|
-
this.restoredMessages = interruptedMessages;
|
|
287
|
-
this.finalizePromise = Promise.resolve();
|
|
288
|
-
}
|
|
289
|
-
const config = loadConfig();
|
|
290
|
-
const authToken = config.machineToken ?? config.accessToken;
|
|
291
|
-
if (!authToken) {
|
|
292
|
-
this.emit('agentEvent', {
|
|
293
|
-
state: 'error',
|
|
294
|
-
runId: this.runId,
|
|
295
|
-
seq: ++this.seq,
|
|
296
|
-
message: '未配对,请先运行 shennian 完成机器配对',
|
|
297
|
-
});
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
this.authToken = authToken;
|
|
301
|
-
this.proxyUrl = (config.serverUrl ?? SERVERS.cn.url).replace(/\/$/, '');
|
|
302
|
-
const managedProviderConfig = getManagedAgentProviderConfig('pi');
|
|
303
|
-
const dashscopeKey = config.apiKeys?.dashscope?.trim();
|
|
304
|
-
this.providerConfig = managedProviderConfig ?? (dashscopeKey
|
|
305
|
-
? { agent: 'pi', token: dashscopeKey, updatedAt: '' }
|
|
306
|
-
: undefined);
|
|
307
|
-
this.runId = randomUUID();
|
|
308
|
-
this.seq = 0;
|
|
309
|
-
this.emittedLengths.clear();
|
|
310
|
-
this.terminalState = 'open';
|
|
311
|
-
if (!this.agent) {
|
|
312
|
-
this.initAgent();
|
|
313
|
-
}
|
|
314
|
-
await this.finalizePromise.catch(() => { });
|
|
315
|
-
this.agent?.setModel(createPiModel(modelId ?? PI_DEFAULT_MODEL_ID));
|
|
316
|
-
this.pendingBaseMessages = cloneMessages(this.agent?.state.messages ?? []);
|
|
317
|
-
const runId = this.runId;
|
|
318
|
-
const sendAccepted = new Promise((resolve, reject) => {
|
|
319
|
-
this.pendingSendStart = { runId, resolve, reject };
|
|
320
|
-
});
|
|
321
|
-
this.emit('agentEvent', { state: 'init', runId, seq: ++this.seq });
|
|
322
|
-
void this.agent.prompt(text)
|
|
323
|
-
.then(async () => {
|
|
324
|
-
if (generation !== this.sendGeneration)
|
|
325
|
-
return;
|
|
326
|
-
this.resolvePendingSendStart(runId);
|
|
327
|
-
await this.finalizePromise.catch(() => { });
|
|
328
|
-
})
|
|
329
|
-
.catch((err) => {
|
|
330
|
-
if (generation !== this.sendGeneration)
|
|
331
|
-
return;
|
|
332
|
-
this.rejectPendingSendStart(runId, err);
|
|
333
|
-
if (this.terminalState !== 'open')
|
|
334
|
-
return;
|
|
335
|
-
this.terminalState = 'error';
|
|
336
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
337
|
-
if (message.includes('429') || message.includes('daily_quota_exceeded') || message.includes('nian_quota_exceeded')) {
|
|
338
|
-
this.emit('agentEvent', {
|
|
339
|
-
state: 'error',
|
|
340
|
-
runId,
|
|
341
|
-
seq: ++this.seq,
|
|
342
|
-
message: 'Nian 今日额度已用完,次日自动恢复。',
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
this.emit('agentEvent', {
|
|
347
|
-
state: 'error',
|
|
348
|
-
runId,
|
|
349
|
-
seq: ++this.seq,
|
|
350
|
-
message,
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
await sendAccepted;
|
|
355
|
-
}
|
|
356
|
-
// ── Agent lifecycle ──────────────────────────────────────────────────────────
|
|
357
|
-
initAgent() {
|
|
358
|
-
const workDir = this.workDir ?? process.cwd();
|
|
359
|
-
const tools = makeTools(workDir, this.extraEnv);
|
|
360
|
-
const agentsMdInstructions = loadAgentsMdInstructions(workDir);
|
|
361
|
-
const agent = new Agent({
|
|
362
|
-
initialState: {
|
|
363
|
-
systemPrompt: [
|
|
364
|
-
SYSTEM_PROMPT,
|
|
365
|
-
agentsMdInstructions,
|
|
366
|
-
`当前工作目录:${workDir}`,
|
|
367
|
-
buildExternalChannelInstructions(this.externalChannel, workDir, this.shennianSessionId ?? undefined),
|
|
368
|
-
].filter(Boolean).join('\n\n'),
|
|
369
|
-
model: createPiModel(),
|
|
370
|
-
tools,
|
|
371
|
-
},
|
|
372
|
-
streamFn: (model, context, options) => {
|
|
373
|
-
const providerConfig = this.providerConfig;
|
|
374
|
-
if (providerConfig?.token) {
|
|
375
|
-
return streamOpenAICompletions(createPiModel(model.id, {
|
|
376
|
-
provider: 'dashscope',
|
|
377
|
-
baseUrl: providerConfig.baseUrl || DASHSCOPE_COMPATIBLE_BASE_URL,
|
|
378
|
-
compat: {
|
|
379
|
-
supportsDeveloperRole: false,
|
|
380
|
-
supportsStore: false,
|
|
381
|
-
supportsReasoningEffort: false,
|
|
382
|
-
maxTokensField: 'max_tokens',
|
|
383
|
-
thinkingFormat: 'qwen',
|
|
384
|
-
},
|
|
385
|
-
}), context, {
|
|
386
|
-
...options,
|
|
387
|
-
apiKey: providerConfig.token,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
return streamProxy(model, context, {
|
|
391
|
-
...options,
|
|
392
|
-
authToken: this.authToken,
|
|
393
|
-
proxyUrl: this.proxyUrl,
|
|
394
|
-
});
|
|
395
|
-
},
|
|
396
|
-
transformContext: (messages) => this.compressContext(messages),
|
|
397
|
-
});
|
|
398
|
-
this.agent = agent;
|
|
399
|
-
if (this.restoredMessages.length > 0) {
|
|
400
|
-
agent.replaceMessages(cloneMessages(this.restoredMessages));
|
|
401
|
-
}
|
|
402
|
-
else if (this.cachedSummary) {
|
|
403
|
-
agent.appendMessage({
|
|
404
|
-
role: 'user',
|
|
405
|
-
content: [{ type: 'text', text: `[之前的对话摘要]\n${this.cachedSummary}` }],
|
|
406
|
-
timestamp: Date.now(),
|
|
407
|
-
});
|
|
408
|
-
agent.appendMessage({
|
|
409
|
-
role: 'assistant',
|
|
410
|
-
content: [{ type: 'text', text: '好的,我已了解之前的对话上下文,请继续。' }],
|
|
411
|
-
timestamp: Date.now(),
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
agent.subscribe((evt) => {
|
|
415
|
-
if (this.agent !== agent)
|
|
416
|
-
return;
|
|
417
|
-
if (this.terminalState !== 'open')
|
|
418
|
-
return;
|
|
419
|
-
const seq = ++this.seq;
|
|
420
|
-
const runId = this.runId;
|
|
421
|
-
switch (evt.type) {
|
|
422
|
-
case 'agent_start':
|
|
423
|
-
this.resolvePendingSendStart(runId);
|
|
424
|
-
this.emit('agentEvent', { state: 'start', runId, seq });
|
|
425
|
-
break;
|
|
426
|
-
case 'message_update': {
|
|
427
|
-
const msg = evt.message;
|
|
428
|
-
if (msg.role !== 'assistant')
|
|
429
|
-
break;
|
|
430
|
-
const content = msg.content ?? [];
|
|
431
|
-
for (let i = 0; i < content.length; i++) {
|
|
432
|
-
const c = content[i];
|
|
433
|
-
if (c.type === 'text' && c.text) {
|
|
434
|
-
const key = `text:${i}`;
|
|
435
|
-
const prev = this.emittedLengths.get(key) ?? 0;
|
|
436
|
-
if (c.text.length > prev) {
|
|
437
|
-
const delta = c.text.slice(prev);
|
|
438
|
-
this.emittedLengths.set(key, c.text.length);
|
|
439
|
-
this.emit('agentEvent', { state: 'delta', runId, seq, text: delta });
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
else if (c.type === 'thinking' && c.thinking) {
|
|
443
|
-
const key = `thinking:${i}`;
|
|
444
|
-
const prev = this.emittedLengths.get(key) ?? 0;
|
|
445
|
-
if (c.thinking.length > prev) {
|
|
446
|
-
const delta = c.thinking.slice(prev);
|
|
447
|
-
this.emittedLengths.set(key, c.thinking.length);
|
|
448
|
-
this.emit('agentEvent', { state: 'delta', runId, seq, text: delta, thinking: true });
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
case 'tool_execution_start':
|
|
455
|
-
this.emit('agentEvent', {
|
|
456
|
-
state: 'tool-call',
|
|
457
|
-
runId,
|
|
458
|
-
seq,
|
|
459
|
-
name: evt.toolName,
|
|
460
|
-
args: evt.args,
|
|
461
|
-
});
|
|
462
|
-
break;
|
|
463
|
-
case 'tool_execution_end':
|
|
464
|
-
this.emit('agentEvent', {
|
|
465
|
-
state: 'tool-result',
|
|
466
|
-
runId,
|
|
467
|
-
seq,
|
|
468
|
-
name: evt.toolName,
|
|
469
|
-
result: typeof evt.result === 'string' ? evt.result : JSON.stringify(evt.result),
|
|
470
|
-
});
|
|
471
|
-
break;
|
|
472
|
-
case 'agent_end': {
|
|
473
|
-
this.terminalState = 'final';
|
|
474
|
-
const msgs = evt.messages;
|
|
475
|
-
const lastAssistant = [...msgs].reverse().find((m) => m.role === 'assistant');
|
|
476
|
-
const usage = lastAssistant?.usage;
|
|
477
|
-
this.emit('agentEvent', {
|
|
478
|
-
state: 'final',
|
|
479
|
-
runId,
|
|
480
|
-
seq,
|
|
481
|
-
usage: usage
|
|
482
|
-
? {
|
|
483
|
-
inputTokens: usage.input ?? 0,
|
|
484
|
-
outputTokens: usage.output ?? 0,
|
|
485
|
-
}
|
|
486
|
-
: undefined,
|
|
487
|
-
});
|
|
488
|
-
this.finalizePromise = this.finalizeTurn(msgs);
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
// ── Context compression ────────────────────────────────────────────────────
|
|
495
|
-
/**
|
|
496
|
-
* After each turn, compress old messages and actually replace them in the
|
|
497
|
-
* Agent's internal state. This is the primary compression mechanism.
|
|
498
|
-
*/
|
|
499
|
-
async compressAndReplace() {
|
|
500
|
-
if (!this.agent || this.compressing)
|
|
501
|
-
return;
|
|
502
|
-
const messages = this.agent.state.messages;
|
|
503
|
-
const tokens = estimateTokens(messages);
|
|
504
|
-
if (tokens < CONTEXT_TOKEN_THRESHOLD)
|
|
505
|
-
return;
|
|
506
|
-
const split = this.splitMessages(messages);
|
|
507
|
-
if (!split)
|
|
508
|
-
return;
|
|
509
|
-
const summary = await this.generateSummary(split.toCompress);
|
|
510
|
-
if (!summary)
|
|
511
|
-
return;
|
|
512
|
-
this.cachedSummary = summary;
|
|
513
|
-
this.lastSummaryMsgCount += split.toCompress.length;
|
|
514
|
-
const replacement = [
|
|
515
|
-
{
|
|
516
|
-
role: 'user',
|
|
517
|
-
content: [{ type: 'text', text: `[历史对话摘要]\n${summary}` }],
|
|
518
|
-
timestamp: Date.now(),
|
|
519
|
-
},
|
|
520
|
-
{
|
|
521
|
-
role: 'assistant',
|
|
522
|
-
content: [{ type: 'text', text: '好的,我已了解上下文。' }],
|
|
523
|
-
timestamp: Date.now(),
|
|
524
|
-
},
|
|
525
|
-
...split.toKeep,
|
|
526
|
-
];
|
|
527
|
-
this.agent.replaceMessages(replacement);
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Safety fallback: if messages somehow exceed the threshold at LLM call time
|
|
531
|
-
* (e.g. compressAndReplace hasn't run yet), return a truncated view using
|
|
532
|
-
* the cached summary. Does NOT call LLM — avoids nested async delays.
|
|
533
|
-
*/
|
|
534
|
-
async compressContext(messages) {
|
|
535
|
-
const tokens = estimateTokens(messages);
|
|
536
|
-
if (tokens < CONTEXT_TOKEN_THRESHOLD)
|
|
537
|
-
return messages;
|
|
538
|
-
if (!this.cachedSummary)
|
|
539
|
-
return messages;
|
|
540
|
-
const split = this.splitMessages(messages);
|
|
541
|
-
if (!split)
|
|
542
|
-
return messages;
|
|
543
|
-
return [
|
|
544
|
-
{
|
|
545
|
-
role: 'user',
|
|
546
|
-
content: [{ type: 'text', text: `[历史对话摘要]\n${this.cachedSummary}` }],
|
|
547
|
-
timestamp: Date.now(),
|
|
548
|
-
},
|
|
549
|
-
...split.toKeep,
|
|
550
|
-
];
|
|
551
|
-
}
|
|
552
|
-
splitMessages(messages) {
|
|
553
|
-
const nonSystem = messages.filter((m) => m.role !== 'system');
|
|
554
|
-
if (nonSystem.length <= KEEP_RECENT_MESSAGES)
|
|
555
|
-
return null;
|
|
556
|
-
let splitIdx = nonSystem.length - KEEP_RECENT_MESSAGES;
|
|
557
|
-
while (splitIdx > 0 &&
|
|
558
|
-
nonSystem[splitIdx].role === 'toolResult') {
|
|
559
|
-
splitIdx--;
|
|
560
|
-
}
|
|
561
|
-
if (splitIdx <= 0)
|
|
562
|
-
return null;
|
|
563
|
-
return { toCompress: nonSystem.slice(0, splitIdx), toKeep: nonSystem.slice(splitIdx) };
|
|
564
|
-
}
|
|
565
|
-
async generateSummary(messages) {
|
|
566
|
-
if (!this.authToken || !this.proxyUrl || this.compressing)
|
|
567
|
-
return this.cachedSummary;
|
|
568
|
-
this.compressing = true;
|
|
569
|
-
try {
|
|
570
|
-
const text = messagesToText(messages);
|
|
571
|
-
if (text.length < 100)
|
|
572
|
-
return null;
|
|
573
|
-
const truncated = text.length > 10000 ? text.slice(0, 10000) + '\n...(已截断)' : text;
|
|
574
|
-
return await requestProxySummary(this.proxyUrl, this.authToken, `请将以下对话历史压缩为简洁摘要,保留关键信息:讨论的文件和目录、做出的技术决定、当前任务进度、未完成的工作。摘要控制在 300 字以内。\n\n${truncated}`);
|
|
575
|
-
}
|
|
576
|
-
catch {
|
|
577
|
-
return this.cachedSummary;
|
|
578
|
-
}
|
|
579
|
-
finally {
|
|
580
|
-
this.compressing = false;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// ── Summary persistence ────────────────────────────────────────────────────
|
|
584
|
-
loadSnapshot() {
|
|
585
|
-
if (!this.snapshotPath)
|
|
586
|
-
return;
|
|
587
|
-
try {
|
|
588
|
-
const raw = fs.readFileSync(this.snapshotPath, 'utf-8');
|
|
589
|
-
const data = JSON.parse(raw);
|
|
590
|
-
this.restoredMessages = Array.isArray(data.messages) ? data.messages : [];
|
|
591
|
-
this.cachedSummary = data.summary ?? this.cachedSummary;
|
|
592
|
-
this.lastSummaryMsgCount = data.summarizedCount ?? this.lastSummaryMsgCount;
|
|
593
|
-
}
|
|
594
|
-
catch {
|
|
595
|
-
this.restoredMessages = [];
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
loadSummary() {
|
|
599
|
-
if (!this.summaryPath)
|
|
600
|
-
return;
|
|
601
|
-
try {
|
|
602
|
-
const raw = fs.readFileSync(this.summaryPath, 'utf-8');
|
|
603
|
-
const data = JSON.parse(raw);
|
|
604
|
-
this.cachedSummary = data.summary ?? null;
|
|
605
|
-
this.lastSummaryMsgCount = data.summarizedCount ?? 0;
|
|
606
|
-
}
|
|
607
|
-
catch {
|
|
608
|
-
if (!this.sessionDir)
|
|
609
|
-
return;
|
|
610
|
-
try {
|
|
611
|
-
const legacyPath = path.join(this.sessionDir, LEGACY_SUMMARY_FILENAME);
|
|
612
|
-
const raw = fs.readFileSync(legacyPath, 'utf-8');
|
|
613
|
-
const data = JSON.parse(raw);
|
|
614
|
-
this.cachedSummary = data.summary ?? null;
|
|
615
|
-
this.lastSummaryMsgCount = data.summarizedCount ?? 0;
|
|
616
|
-
}
|
|
617
|
-
catch {
|
|
618
|
-
// File doesn't exist or is corrupt — start fresh
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
saveSummary(messages) {
|
|
623
|
-
if (!this.summaryPath)
|
|
624
|
-
return;
|
|
625
|
-
try {
|
|
626
|
-
fs.mkdirSync(path.dirname(this.summaryPath), { recursive: true });
|
|
627
|
-
const summary = buildRollingSummary(this.cachedSummary, messages);
|
|
628
|
-
this.cachedSummary = summary;
|
|
629
|
-
fs.writeFileSync(this.summaryPath, JSON.stringify({
|
|
630
|
-
version: 1,
|
|
631
|
-
summary,
|
|
632
|
-
summarizedCount: this.lastSummaryMsgCount,
|
|
633
|
-
updatedAt: Date.now(),
|
|
634
|
-
}, null, 2));
|
|
635
|
-
}
|
|
636
|
-
catch {
|
|
637
|
-
// Best-effort persistence
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
saveSnapshot(messages) {
|
|
641
|
-
if (!this.snapshotPath || !this.sessionId || !this.workDir)
|
|
642
|
-
return;
|
|
643
|
-
try {
|
|
644
|
-
fs.mkdirSync(path.dirname(this.snapshotPath), { recursive: true });
|
|
645
|
-
fs.writeFileSync(this.snapshotPath, JSON.stringify({
|
|
646
|
-
version: 1,
|
|
647
|
-
sessionId: this.sessionId,
|
|
648
|
-
workDir: this.workDir,
|
|
649
|
-
summary: this.cachedSummary,
|
|
650
|
-
summarizedCount: this.lastSummaryMsgCount,
|
|
651
|
-
messages,
|
|
652
|
-
updatedAt: Date.now(),
|
|
653
|
-
}, null, 2));
|
|
654
|
-
this.restoredMessages = cloneMessages(messages);
|
|
655
|
-
}
|
|
656
|
-
catch {
|
|
657
|
-
// Best-effort persistence
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
appendMessagesLog(messages) {
|
|
661
|
-
if (!this.messagesPath || messages.length === 0)
|
|
662
|
-
return;
|
|
663
|
-
try {
|
|
664
|
-
fs.mkdirSync(path.dirname(this.messagesPath), { recursive: true });
|
|
665
|
-
const lines = messages.map((message) => JSON.stringify(message)).join('\n') + '\n';
|
|
666
|
-
fs.appendFileSync(this.messagesPath, lines, 'utf-8');
|
|
667
|
-
}
|
|
668
|
-
catch {
|
|
669
|
-
// Best-effort persistence
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
getTurnMessages(finalMessages) {
|
|
673
|
-
const prefixLength = longestCommonPrefixLength(this.pendingBaseMessages, finalMessages);
|
|
674
|
-
return cloneMessages(finalMessages.slice(prefixLength));
|
|
675
|
-
}
|
|
676
|
-
async finalizeTurn(finalMessages) {
|
|
677
|
-
const clonedFinalMessages = cloneMessages(finalMessages);
|
|
678
|
-
const newMessages = this.getTurnMessages(clonedFinalMessages);
|
|
679
|
-
this.appendMessagesLog(newMessages);
|
|
680
|
-
await this.compressAndReplace();
|
|
681
|
-
const currentMessages = this.agent
|
|
682
|
-
? cloneMessages(this.agent.state.messages)
|
|
683
|
-
: clonedFinalMessages;
|
|
684
|
-
if (!this.cachedSummary) {
|
|
685
|
-
this.cachedSummary = buildRollingSummary(null, clonedFinalMessages);
|
|
686
|
-
}
|
|
687
|
-
this.saveSummary(currentMessages);
|
|
688
|
-
this.saveSnapshot(currentMessages);
|
|
689
|
-
}
|
|
690
|
-
async resume(_agentSessionId) {
|
|
691
|
-
this.loadSnapshot();
|
|
692
|
-
this.loadSummary();
|
|
693
|
-
if (this.agent && this.restoredMessages.length > 0) {
|
|
694
|
-
this.agent.replaceMessages(cloneMessages(this.restoredMessages));
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
async stop() {
|
|
698
|
-
this.rejectPendingSendStart(this.runId, new Error('Pi run stopped before dispatch completed'));
|
|
699
|
-
await this.finalizePromise.catch(() => { });
|
|
700
|
-
if (this.agent) {
|
|
701
|
-
const currentMessages = cloneMessages(this.agent.state.messages ?? []);
|
|
702
|
-
this.saveSummary(currentMessages);
|
|
703
|
-
this.saveSnapshot(currentMessages);
|
|
704
|
-
}
|
|
705
|
-
this.agent?.abort();
|
|
706
|
-
this.agent = null;
|
|
707
|
-
}
|
|
708
|
-
resolvePendingSendStart(runId) {
|
|
709
|
-
if (!this.pendingSendStart || this.pendingSendStart.runId !== runId)
|
|
710
|
-
return;
|
|
711
|
-
const { resolve } = this.pendingSendStart;
|
|
712
|
-
this.pendingSendStart = null;
|
|
713
|
-
resolve();
|
|
714
|
-
}
|
|
715
|
-
rejectPendingSendStart(runId, error) {
|
|
716
|
-
if (!this.pendingSendStart || this.pendingSendStart.runId !== runId)
|
|
717
|
-
return;
|
|
718
|
-
const { reject } = this.pendingSendStart;
|
|
719
|
-
this.pendingSendStart = null;
|
|
720
|
-
reject(error);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
registerAgent('pi', () => new PiAdapter());
|
|
1
|
+
import{randomUUID as C}from"node:crypto";import c from"node:fs";import g from"node:path";import{execFile as _}from"node:child_process";import{promisify as b}from"node:util";import{Agent as A,streamProxy as T}from"@mariozechner/pi-agent-core";import{streamOpenAICompletions as I}from"@mariozechner/pi-ai/openai-completions";import{Type as d}from"@sinclair/typebox";import{AgentAdapter as D,registerAgent as j}from"./adapter.js";import{buildExternalChannelInstructions as F}from"./external-channel-instructions.js";import{loadConfig as L}from"../config/index.js";import{getManagedAgentProviderConfig as O}from"./config-status.js";import{SERVERS as $}from"../region.js";import{buildRollingSummary as E,buildShellCommandSpec as P,cloneMessages as y,CONTEXT_TOKEN_THRESHOLD as v,createPiModel as M,estimateTokens as w,getSessionDir as N,KEEP_RECENT_MESSAGES as k,LEGACY_SUMMARY_FILENAME as q,loadAgentsMdInstructions as z,longestCommonPrefixLength as R,MESSAGES_FILENAME as U,messagesToText as B,PI_DEFAULT_MODEL_ID as K,requestProxySummary as G,SNAPSHOT_FILENAME as J,SYSTEM_PROMPT as H,SUMMARY_FILENAME as Y}from"./pi-context.js";import{buildShellCommandSpec as yt}from"./pi-context.js";const W=b(_),V="https://dashscope.aliyuncs.com/compatible-mode/v1";function f(p,t){return g.resolve(p,t)}async function X(p,t,e,n){const i=P(t),{stdout:s,stderr:a}=await W(i.file,i.args,{cwd:p,env:{...process.env,...e},timeout:3e4,signal:n,maxBuffer:1024*1024,windowsHide:!0});return{stdout:s,stderr:a,shell:i.shell}}function Q(p,t={}){return[{name:"read_file",label:"\u8BFB\u53D6\u6587\u4EF6",description:"Read the full content of a file at the given path.",parameters:d.Object({path:d.String({description:"Absolute or relative file path"})}),async execute(e,{path:n}){let i="";try{return i=f(p,n),{content:[{type:"text",text:c.readFileSync(i,"utf-8")}],details:{path:i}}}catch(s){return{content:[{type:"text",text:`Error: ${s instanceof Error?s.message:String(s)}`}],details:{path:i}}}}},{name:"write_file",label:"\u5199\u5165\u6587\u4EF6",description:"Write content to a file, creating it or overwriting if it exists.",parameters:d.Object({path:d.String({description:"Absolute or relative file path"}),content:d.String({description:"File content to write"})}),async execute(e,{path:n,content:i}){let s="";try{return s=f(p,n),c.mkdirSync(g.dirname(s),{recursive:!0}),c.writeFileSync(s,i,"utf-8"),{content:[{type:"text",text:`Written ${i.length} bytes to ${s}`}],details:{path:s}}}catch(a){return{content:[{type:"text",text:`Error: ${a instanceof Error?a.message:String(a)}`}],details:{path:s}}}}},{name:"list_directory",label:"\u5217\u51FA\u76EE\u5F55",description:"List files and directories at the given path.",parameters:d.Object({path:d.String({description:"Directory path to list",default:"."})}),async execute(e,{path:n}){let i="";try{return i=f(p,n),{content:[{type:"text",text:c.readdirSync(i,{withFileTypes:!0}).map(o=>`${o.isDirectory()?"d":"f"} ${o.name}`).join(`
|
|
2
|
+
`)||"(empty)"}],details:{path:i}}}catch(s){return{content:[{type:"text",text:`Error: ${s instanceof Error?s.message:String(s)}`}],details:{path:i}}}}},{name:"bash",label:"\u6267\u884C\u547D\u4EE4",description:"Execute a shell command in the working directory. Uses PowerShell on Windows and bash on macOS/Linux. Timeout: 30s.",parameters:d.Object({command:d.String({description:"Shell command to execute"})}),async execute(e,{command:n},i){try{const{stdout:s,stderr:a,shell:o}=await X(p,n,t,i);return{content:[{type:"text",text:[s,a].filter(Boolean).join(`
|
|
3
|
+
---stderr---
|
|
4
|
+
`)||"(no output)"}],details:{command:n,shell:o}}}catch(s){return{content:[{type:"text",text:`Error: ${s instanceof Error?s.message:String(s)}`}],details:{command:n,shell:P(n).shell}}}}},{name:"read_image",label:"\u8BFB\u53D6\u56FE\u7247",description:"Read an image file and return its content for visual analysis. Supports png, jpg, jpeg, gif, webp. Max 10 MB.",parameters:d.Object({path:d.String({description:"Absolute or relative path to the image file"})}),async execute(e,{path:n}){let i;try{i=f(p,n)}catch(r){return{content:[{type:"text",text:`Error: ${r instanceof Error?r.message:String(r)}`}],details:{path:n}}}const s=g.extname(i).toLowerCase().slice(1),o={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp"}[s];if(!o)return{content:[{type:"text",text:`Error: unsupported image type ".${s}". Supported: png, jpg, jpeg, gif, webp`}],details:{path:i}};try{const r=c.statSync(i);return r.size>10*1024*1024?{content:[{type:"text",text:`Error: image too large (${(r.size/1024/1024).toFixed(1)} MB, max 10 MB)`}],details:{path:i}}:{content:[{type:"image",data:c.readFileSync(i).toString("base64"),mimeType:o}],details:{path:i,mimeType:o,size:r.size}}}catch(r){return{content:[{type:"text",text:`Error: ${r instanceof Error?r.message:String(r)}`}],details:{path:i}}}}}]}class Z extends D{type="pi";agent=null;sessionId=null;workDir=null;seq=0;runId="";emittedLengths=new Map;terminalState="open";authToken=null;proxyUrl=null;providerConfig;sessionDir=null;messagesPath=null;snapshotPath=null;summaryPath=null;cachedSummary=null;restoredMessages=[];compressing=!1;lastSummaryMsgCount=0;pendingBaseMessages=[];finalizePromise=Promise.resolve();sendGeneration=0;externalChannel=null;shennianSessionId=null;extraEnv={};pendingSendStart=null;configure(t){this.shennianSessionId=t.sessionId??null,this.externalChannel=t.externalChannel??null,this.extraEnv=t.env??{}}async start(t,e,n){this.sessionId=t,this.workDir=e,this.seq=0,this.sessionDir=N(t),this.messagesPath=g.join(this.sessionDir,U),this.snapshotPath=g.join(this.sessionDir,J),this.summaryPath=g.join(this.sessionDir,Y),this.loadSnapshot(),this.loadSummary()}async send(t,e){const n=++this.sendGeneration,i=this.agent&&this.terminalState==="open"?y(this.pendingBaseMessages):null;i&&(this.agent?.abort(),this.agent=null,this.restoredMessages=i,this.finalizePromise=Promise.resolve());const s=L(),a=s.machineToken??s.accessToken;if(!a){this.emit("agentEvent",{state:"error",runId:this.runId,seq:++this.seq,message:"\u672A\u914D\u5BF9\uFF0C\u8BF7\u5148\u8FD0\u884C shennian \u5B8C\u6210\u673A\u5668\u914D\u5BF9"});return}this.authToken=a,this.proxyUrl=(s.serverUrl??$.cn.url).replace(/\/$/,"");const o=O("pi"),r=s.apiKeys?.dashscope?.trim();this.providerConfig=o??(r?{agent:"pi",token:r,updatedAt:""}:void 0),this.runId=C(),this.seq=0,this.emittedLengths.clear(),this.terminalState="open",this.agent||this.initAgent(),await this.finalizePromise.catch(()=>{}),this.agent?.setModel(M(e??K)),this.pendingBaseMessages=y(this.agent?.state.messages??[]);const u=this.runId,l=new Promise((h,m)=>{this.pendingSendStart={runId:u,resolve:h,reject:m}});this.emit("agentEvent",{state:"init",runId:u,seq:++this.seq}),this.agent.prompt(t).then(async()=>{n===this.sendGeneration&&(this.resolvePendingSendStart(u),await this.finalizePromise.catch(()=>{}))}).catch(h=>{if(n!==this.sendGeneration||(this.rejectPendingSendStart(u,h),this.terminalState!=="open"))return;this.terminalState="error";const m=h instanceof Error?h.message:String(h);m.includes("429")||m.includes("daily_quota_exceeded")||m.includes("nian_quota_exceeded")?this.emit("agentEvent",{state:"error",runId:u,seq:++this.seq,message:"Nian \u4ECA\u65E5\u989D\u5EA6\u5DF2\u7528\u5B8C\uFF0C\u6B21\u65E5\u81EA\u52A8\u6062\u590D\u3002"}):this.emit("agentEvent",{state:"error",runId:u,seq:++this.seq,message:m})}),await l}initAgent(){const t=this.workDir??process.cwd(),e=Q(t,this.extraEnv),n=z(t),i=new A({initialState:{systemPrompt:[H,n,`\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF1A${t}`,F(this.externalChannel,t,this.shennianSessionId??void 0)].filter(Boolean).join(`
|
|
5
|
+
|
|
6
|
+
`),model:M(),tools:e},streamFn:(s,a,o)=>{const r=this.providerConfig;return r?.token?I(M(s.id,{provider:"dashscope",baseUrl:r.baseUrl||V,compat:{supportsDeveloperRole:!1,supportsStore:!1,supportsReasoningEffort:!1,maxTokensField:"max_tokens",thinkingFormat:"qwen"}}),a,{...o,apiKey:r.token}):T(s,a,{...o,authToken:this.authToken,proxyUrl:this.proxyUrl})},transformContext:s=>this.compressContext(s)});this.agent=i,this.restoredMessages.length>0?i.replaceMessages(y(this.restoredMessages)):this.cachedSummary&&(i.appendMessage({role:"user",content:[{type:"text",text:`[\u4E4B\u524D\u7684\u5BF9\u8BDD\u6458\u8981]
|
|
7
|
+
${this.cachedSummary}`}],timestamp:Date.now()}),i.appendMessage({role:"assistant",content:[{type:"text",text:"\u597D\u7684\uFF0C\u6211\u5DF2\u4E86\u89E3\u4E4B\u524D\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u8BF7\u7EE7\u7EED\u3002"}],timestamp:Date.now()})),i.subscribe(s=>{if(this.agent!==i||this.terminalState!=="open")return;const a=++this.seq,o=this.runId;switch(s.type){case"agent_start":this.resolvePendingSendStart(o),this.emit("agentEvent",{state:"start",runId:o,seq:a});break;case"message_update":{const r=s.message;if(r.role!=="assistant")break;const u=r.content??[];for(let l=0;l<u.length;l++){const h=u[l];if(h.type==="text"&&h.text){const m=`text:${l}`,S=this.emittedLengths.get(m)??0;if(h.text.length>S){const x=h.text.slice(S);this.emittedLengths.set(m,h.text.length),this.emit("agentEvent",{state:"delta",runId:o,seq:a,text:x})}}else if(h.type==="thinking"&&h.thinking){const m=`thinking:${l}`,S=this.emittedLengths.get(m)??0;if(h.thinking.length>S){const x=h.thinking.slice(S);this.emittedLengths.set(m,h.thinking.length),this.emit("agentEvent",{state:"delta",runId:o,seq:a,text:x,thinking:!0})}}}break}case"tool_execution_start":this.emit("agentEvent",{state:"tool-call",runId:o,seq:a,name:s.toolName,args:s.args});break;case"tool_execution_end":this.emit("agentEvent",{state:"tool-result",runId:o,seq:a,name:s.toolName,result:typeof s.result=="string"?s.result:JSON.stringify(s.result)});break;case"agent_end":{this.terminalState="final";const r=s.messages,l=[...r].reverse().find(h=>h.role==="assistant")?.usage;this.emit("agentEvent",{state:"final",runId:o,seq:a,usage:l?{inputTokens:l.input??0,outputTokens:l.output??0}:void 0}),this.finalizePromise=this.finalizeTurn(r);break}}})}async compressAndReplace(){if(!this.agent||this.compressing)return;const t=this.agent.state.messages;if(w(t)<v)return;const n=this.splitMessages(t);if(!n)return;const i=await this.generateSummary(n.toCompress);if(!i)return;this.cachedSummary=i,this.lastSummaryMsgCount+=n.toCompress.length;const s=[{role:"user",content:[{type:"text",text:`[\u5386\u53F2\u5BF9\u8BDD\u6458\u8981]
|
|
8
|
+
${i}`}],timestamp:Date.now()},{role:"assistant",content:[{type:"text",text:"\u597D\u7684\uFF0C\u6211\u5DF2\u4E86\u89E3\u4E0A\u4E0B\u6587\u3002"}],timestamp:Date.now()},...n.toKeep];this.agent.replaceMessages(s)}async compressContext(t){if(w(t)<v||!this.cachedSummary)return t;const n=this.splitMessages(t);return n?[{role:"user",content:[{type:"text",text:`[\u5386\u53F2\u5BF9\u8BDD\u6458\u8981]
|
|
9
|
+
${this.cachedSummary}`}],timestamp:Date.now()},...n.toKeep]:t}splitMessages(t){const e=t.filter(i=>i.role!=="system");if(e.length<=k)return null;let n=e.length-k;for(;n>0&&e[n].role==="toolResult";)n--;return n<=0?null:{toCompress:e.slice(0,n),toKeep:e.slice(n)}}async generateSummary(t){if(!this.authToken||!this.proxyUrl||this.compressing)return this.cachedSummary;this.compressing=!0;try{const e=B(t);if(e.length<100)return null;const n=e.length>1e4?e.slice(0,1e4)+`
|
|
10
|
+
...(\u5DF2\u622A\u65AD)`:e;return await G(this.proxyUrl,this.authToken,`\u8BF7\u5C06\u4EE5\u4E0B\u5BF9\u8BDD\u5386\u53F2\u538B\u7F29\u4E3A\u7B80\u6D01\u6458\u8981\uFF0C\u4FDD\u7559\u5173\u952E\u4FE1\u606F\uFF1A\u8BA8\u8BBA\u7684\u6587\u4EF6\u548C\u76EE\u5F55\u3001\u505A\u51FA\u7684\u6280\u672F\u51B3\u5B9A\u3001\u5F53\u524D\u4EFB\u52A1\u8FDB\u5EA6\u3001\u672A\u5B8C\u6210\u7684\u5DE5\u4F5C\u3002\u6458\u8981\u63A7\u5236\u5728 300 \u5B57\u4EE5\u5185\u3002
|
|
11
|
+
|
|
12
|
+
${n}`)}catch{return this.cachedSummary}finally{this.compressing=!1}}loadSnapshot(){if(this.snapshotPath)try{const t=c.readFileSync(this.snapshotPath,"utf-8"),e=JSON.parse(t);this.restoredMessages=Array.isArray(e.messages)?e.messages:[],this.cachedSummary=e.summary??this.cachedSummary,this.lastSummaryMsgCount=e.summarizedCount??this.lastSummaryMsgCount}catch{this.restoredMessages=[]}}loadSummary(){if(this.summaryPath)try{const t=c.readFileSync(this.summaryPath,"utf-8"),e=JSON.parse(t);this.cachedSummary=e.summary??null,this.lastSummaryMsgCount=e.summarizedCount??0}catch{if(!this.sessionDir)return;try{const t=g.join(this.sessionDir,q),e=c.readFileSync(t,"utf-8"),n=JSON.parse(e);this.cachedSummary=n.summary??null,this.lastSummaryMsgCount=n.summarizedCount??0}catch{}}}saveSummary(t){if(this.summaryPath)try{c.mkdirSync(g.dirname(this.summaryPath),{recursive:!0});const e=E(this.cachedSummary,t);this.cachedSummary=e,c.writeFileSync(this.summaryPath,JSON.stringify({version:1,summary:e,summarizedCount:this.lastSummaryMsgCount,updatedAt:Date.now()},null,2))}catch{}}saveSnapshot(t){if(!(!this.snapshotPath||!this.sessionId||!this.workDir))try{c.mkdirSync(g.dirname(this.snapshotPath),{recursive:!0}),c.writeFileSync(this.snapshotPath,JSON.stringify({version:1,sessionId:this.sessionId,workDir:this.workDir,summary:this.cachedSummary,summarizedCount:this.lastSummaryMsgCount,messages:t,updatedAt:Date.now()},null,2)),this.restoredMessages=y(t)}catch{}}appendMessagesLog(t){if(!(!this.messagesPath||t.length===0))try{c.mkdirSync(g.dirname(this.messagesPath),{recursive:!0});const e=t.map(n=>JSON.stringify(n)).join(`
|
|
13
|
+
`)+`
|
|
14
|
+
`;c.appendFileSync(this.messagesPath,e,"utf-8")}catch{}}getTurnMessages(t){const e=R(this.pendingBaseMessages,t);return y(t.slice(e))}async finalizeTurn(t){const e=y(t),n=this.getTurnMessages(e);this.appendMessagesLog(n),await this.compressAndReplace();const i=this.agent?y(this.agent.state.messages):e;this.cachedSummary||(this.cachedSummary=E(null,e)),this.saveSummary(i),this.saveSnapshot(i)}async resume(t){this.loadSnapshot(),this.loadSummary(),this.agent&&this.restoredMessages.length>0&&this.agent.replaceMessages(y(this.restoredMessages))}async stop(){if(this.rejectPendingSendStart(this.runId,new Error("Pi run stopped before dispatch completed")),await this.finalizePromise.catch(()=>{}),this.agent){const t=y(this.agent.state.messages??[]);this.saveSummary(t),this.saveSnapshot(t)}this.agent?.abort(),this.agent=null}resolvePendingSendStart(t){if(!this.pendingSendStart||this.pendingSendStart.runId!==t)return;const{resolve:e}=this.pendingSendStart;this.pendingSendStart=null,e()}rejectPendingSendStart(t,e){if(!this.pendingSendStart||this.pendingSendStart.runId!==t)return;const{reject:n}=this.pendingSendStart;this.pendingSendStart=null,n(e)}}j("pi",()=>new Z);export{Z as PiAdapter,yt as buildShellCommandSpec};
|