shennian 0.2.88 → 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 +22 -0
- 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.d.ts +6 -0
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.d.ts +35 -0
- package/dist/src/agents/codex-control.js +2 -0
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.d.ts +8 -0
- package/dist/src/agents/codex.js +15 -863
- 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.d.ts +4 -1
- 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.d.ts +1 -0
- package/dist/src/channels/runtime.js +5 -533
- package/dist/src/channels/secret-registry.d.ts +1 -0
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
- package/dist/src/channels/wechat-channel/anchor.js +1 -0
- package/dist/src/channels/wechat-channel/client.d.ts +74 -0
- package/dist/src/channels/wechat-channel/client.js +1 -0
- package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
- package/dist/src/channels/wechat-channel/cooldown.js +1 -0
- package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -0
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +37 -0
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -0
- package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
- package/dist/src/channels/wechat-channel/helper-client.js +3 -0
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -0
- package/dist/src/channels/wechat-channel/index.d.ts +17 -0
- package/dist/src/channels/wechat-channel/index.js +1 -0
- package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
- package/dist/src/channels/wechat-channel/ledger.js +1 -0
- package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -0
- package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
- package/dist/src/channels/wechat-channel/message-key.js +1 -0
- package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
- package/dist/src/channels/wechat-channel/observer.js +1 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +69 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -0
- 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.d.ts +37 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -0
- package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
- package/dist/src/channels/wechat-channel/runner.js +1 -0
- package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
- package/dist/src/channels/wechat-channel/runtime.js +1 -0
- package/dist/src/channels/wechat-channel/scheduler.d.ts +35 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -0
- 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.d.ts +21 -0
- package/dist/src/channels/wechat-rpa.js +6 -1022
- 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 -389
- 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.d.ts +10 -0
- package/dist/src/fs/text-decoder.js +1 -0
- 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 -1003
- 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.d.ts +10 -0
- package/dist/src/native-fusion/service.js +2 -198
- 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 -733
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -747
- package/dist/src/session/handlers/session-refresh.js +1 -35
- 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.d.ts +3 -0
- package/dist/src/session/handlers/tool-detail.js +1 -0
- package/dist/src/session/manager.d.ts +3 -0
- package/dist/src/session/manager.js +1 -261
- 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.d.ts +4 -0
- 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/scripts/wechat-rpa-download-candidates.mjs +0 -105
|
@@ -1,317 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { resolveShennianPath } from '../config/index.js';
|
|
6
|
-
import { mergeProjectedSessions } from './projection.js';
|
|
7
|
-
import { materializeRemoteChatAttachments } from './remote-attachments.js';
|
|
8
|
-
const QUEUE_FILE = resolveShennianPath('chat-queue.json');
|
|
9
|
-
function emptyQueue() {
|
|
10
|
-
return { sessions: {} };
|
|
11
|
-
}
|
|
12
|
-
function nowIso() {
|
|
13
|
-
return new Date().toISOString();
|
|
14
|
-
}
|
|
15
|
-
function forwardedReqId(reqId) {
|
|
16
|
-
return `enqueue-send-${reqId}-${randomUUID()}`;
|
|
17
|
-
}
|
|
18
|
-
function readQueue() {
|
|
19
|
-
try {
|
|
20
|
-
const parsed = JSON.parse(fs.readFileSync(QUEUE_FILE, 'utf-8'));
|
|
21
|
-
return {
|
|
22
|
-
sessions: parsed.sessions && typeof parsed.sessions === 'object' ? parsed.sessions : {},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return emptyQueue();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function writeQueue(queue) {
|
|
30
|
-
fs.mkdirSync(resolveShennianPath(''), { recursive: true });
|
|
31
|
-
fs.writeFileSync(QUEUE_FILE, JSON.stringify(queue, null, 2));
|
|
32
|
-
}
|
|
33
|
-
function normalizeAttachments(value) {
|
|
34
|
-
if (!Array.isArray(value))
|
|
35
|
-
return undefined;
|
|
36
|
-
const attachments = value
|
|
37
|
-
.map((item) => {
|
|
38
|
-
if (!item || typeof item !== 'object')
|
|
39
|
-
return null;
|
|
40
|
-
const entry = item;
|
|
41
|
-
const attachment = {
|
|
42
|
-
path: typeof entry.path === 'string' ? entry.path : '',
|
|
43
|
-
name: typeof entry.name === 'string' ? entry.name : '',
|
|
44
|
-
mimeType: typeof entry.mimeType === 'string' ? entry.mimeType : '',
|
|
45
|
-
...(typeof entry.previewData === 'string' && entry.previewData.trim() ? { previewData: entry.previewData.trim() } : {}),
|
|
46
|
-
};
|
|
47
|
-
return attachment.path && attachment.name && attachment.mimeType ? attachment : null;
|
|
48
|
-
})
|
|
49
|
-
.filter((item) => item != null);
|
|
50
|
-
return attachments.length ? attachments : undefined;
|
|
51
|
-
}
|
|
52
|
-
function queueMessageFromParams(params) {
|
|
53
|
-
const timestamp = nowIso();
|
|
54
|
-
return {
|
|
55
|
-
id: params.queueMessageId || params.clientMessageId || `queue-${randomUUID()}`,
|
|
56
|
-
sessionId: params.sessionId,
|
|
57
|
-
text: params.text,
|
|
58
|
-
agentType: params.agentType,
|
|
59
|
-
workDir: params.workDir,
|
|
60
|
-
agentSessionId: params.agentSessionId ?? null,
|
|
61
|
-
modelId: params.modelId ?? null,
|
|
62
|
-
reasoningEffort: params.reasoningEffort ?? null,
|
|
63
|
-
clientMessageId: params.clientMessageId ?? null,
|
|
64
|
-
attachments: normalizeAttachments(params.attachments),
|
|
65
|
-
externalChannel: params.externalChannel ?? null,
|
|
66
|
-
replyTarget: params.replyTarget ?? null,
|
|
67
|
-
origin: params.origin,
|
|
68
|
-
createdAt: timestamp,
|
|
69
|
-
updatedAt: timestamp,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
export class ChatQueueManager {
|
|
73
|
-
opts;
|
|
74
|
-
draining = new Set();
|
|
75
|
-
constructor(opts) {
|
|
76
|
-
this.opts = opts;
|
|
77
|
-
const timer = setTimeout(() => this.drainIdleQueues(), 0);
|
|
78
|
-
timer.unref?.();
|
|
79
|
-
}
|
|
80
|
-
getSnapshot(sessionId) {
|
|
81
|
-
return {
|
|
82
|
-
sessionId,
|
|
83
|
-
busy: Boolean(this.opts.getRuntime().sessions.get(sessionId)?.currentRunId),
|
|
84
|
-
pending: readQueue().sessions[sessionId] ?? [],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
async handleEnqueue(req) {
|
|
88
|
-
const runtime = this.opts.getRuntime();
|
|
89
|
-
const params = req.params;
|
|
90
|
-
mergeProjectedSessions(params.sessionListProjection);
|
|
91
|
-
if (!params.sessionId || !params.text || !params.agentType || !params.workDir) {
|
|
92
|
-
runtime.client.sendRes({
|
|
93
|
-
type: 'res',
|
|
94
|
-
id: req.id,
|
|
95
|
-
ok: false,
|
|
96
|
-
error: 'sessionId, text, agentType and workDir are required',
|
|
97
|
-
});
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const normalizedAttachments = normalizeAttachments(params.attachments);
|
|
101
|
-
const materialized = normalizedAttachments?.length
|
|
102
|
-
? await materializeRemoteChatAttachments({ text: params.text, attachments: normalizedAttachments, workDir: params.workDir })
|
|
103
|
-
: { text: params.text, attachments: normalizedAttachments, localized: false };
|
|
104
|
-
const active = runtime.sessions.get(params.sessionId);
|
|
105
|
-
const isBusy = Boolean(active?.currentRunId);
|
|
106
|
-
if (!isBusy && !(readQueue().sessions[params.sessionId]?.length)) {
|
|
107
|
-
await this.opts.dispatchReq({
|
|
108
|
-
...req,
|
|
109
|
-
id: forwardedReqId(req.id),
|
|
110
|
-
method: 'chat.send',
|
|
111
|
-
params: {
|
|
112
|
-
...params,
|
|
113
|
-
text: materialized.text,
|
|
114
|
-
responseId: req.id,
|
|
115
|
-
clientMessageId: params.clientMessageId ?? params.queueMessageId,
|
|
116
|
-
waitForDispatch: true,
|
|
117
|
-
attachments: materialized.attachments,
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const queue = readQueue();
|
|
123
|
-
const message = queueMessageFromParams({
|
|
124
|
-
...params,
|
|
125
|
-
text: materialized.text,
|
|
126
|
-
attachments: materialized.attachments,
|
|
127
|
-
});
|
|
128
|
-
queue.sessions[params.sessionId] = [...(queue.sessions[params.sessionId] ?? []), message];
|
|
129
|
-
writeQueue(queue);
|
|
130
|
-
this.broadcast(params.sessionId);
|
|
131
|
-
runtime.client.sendRes({
|
|
132
|
-
type: 'res',
|
|
133
|
-
id: req.id,
|
|
134
|
-
ok: true,
|
|
135
|
-
payload: {
|
|
136
|
-
queued: true,
|
|
137
|
-
queueMessageId: message.id,
|
|
138
|
-
queue: this.getSnapshot(params.sessionId),
|
|
139
|
-
...(materialized.localized ? { localizedAttachments: true } : {}),
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
if (!isBusy) {
|
|
143
|
-
void this.drainNext(params.sessionId);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async handleGet(req) {
|
|
147
|
-
const runtime = this.opts.getRuntime();
|
|
148
|
-
const params = req.params;
|
|
149
|
-
if (!params.sessionId) {
|
|
150
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'sessionId is required' });
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
runtime.client.sendRes({
|
|
154
|
-
type: 'res',
|
|
155
|
-
id: req.id,
|
|
156
|
-
ok: true,
|
|
157
|
-
payload: { queue: this.getSnapshot(params.sessionId) },
|
|
158
|
-
});
|
|
159
|
-
if (!this.getSnapshot(params.sessionId).busy) {
|
|
160
|
-
void this.drainNext(params.sessionId);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
async handleEdit(req) {
|
|
164
|
-
const runtime = this.opts.getRuntime();
|
|
165
|
-
const params = req.params;
|
|
166
|
-
if (!params.sessionId || !params.queueMessageId || !params.text) {
|
|
167
|
-
runtime.client.sendRes({
|
|
168
|
-
type: 'res',
|
|
169
|
-
id: req.id,
|
|
170
|
-
ok: false,
|
|
171
|
-
error: 'sessionId, queueMessageId and text are required',
|
|
172
|
-
});
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const queue = readQueue();
|
|
176
|
-
const pending = queue.sessions[params.sessionId] ?? [];
|
|
177
|
-
const index = pending.findIndex((message) => message.id === params.queueMessageId);
|
|
178
|
-
if (index < 0) {
|
|
179
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Queued message not found' });
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
pending[index] = {
|
|
183
|
-
...pending[index],
|
|
184
|
-
text: params.text,
|
|
185
|
-
attachments: normalizeAttachments(params.attachments),
|
|
186
|
-
updatedAt: nowIso(),
|
|
187
|
-
};
|
|
188
|
-
queue.sessions[params.sessionId] = pending;
|
|
189
|
-
writeQueue(queue);
|
|
190
|
-
this.broadcast(params.sessionId);
|
|
191
|
-
runtime.client.sendRes({
|
|
192
|
-
type: 'res',
|
|
193
|
-
id: req.id,
|
|
194
|
-
ok: true,
|
|
195
|
-
payload: { queue: this.getSnapshot(params.sessionId) },
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
async handleDelete(req) {
|
|
199
|
-
const runtime = this.opts.getRuntime();
|
|
200
|
-
const params = req.params;
|
|
201
|
-
if (!params.sessionId || !params.queueMessageId) {
|
|
202
|
-
runtime.client.sendRes({
|
|
203
|
-
type: 'res',
|
|
204
|
-
id: req.id,
|
|
205
|
-
ok: false,
|
|
206
|
-
error: 'sessionId and queueMessageId are required',
|
|
207
|
-
});
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const queue = readQueue();
|
|
211
|
-
const pending = queue.sessions[params.sessionId] ?? [];
|
|
212
|
-
const next = pending.filter((message) => message.id !== params.queueMessageId);
|
|
213
|
-
if (next.length === pending.length) {
|
|
214
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Queued message not found' });
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
if (next.length)
|
|
218
|
-
queue.sessions[params.sessionId] = next;
|
|
219
|
-
else
|
|
220
|
-
delete queue.sessions[params.sessionId];
|
|
221
|
-
writeQueue(queue);
|
|
222
|
-
this.broadcast(params.sessionId);
|
|
223
|
-
runtime.client.sendRes({
|
|
224
|
-
type: 'res',
|
|
225
|
-
id: req.id,
|
|
226
|
-
ok: true,
|
|
227
|
-
payload: { queue: this.getSnapshot(params.sessionId) },
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
noteTerminal(sessionId) {
|
|
231
|
-
void this.drainNext(sessionId);
|
|
232
|
-
}
|
|
233
|
-
drainIdleQueues() {
|
|
234
|
-
const queue = readQueue();
|
|
235
|
-
for (const sessionId of Object.keys(queue.sessions)) {
|
|
236
|
-
if (!this.getSnapshot(sessionId).busy) {
|
|
237
|
-
void this.drainNext(sessionId);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
async drainNext(sessionId) {
|
|
242
|
-
if (this.draining.has(sessionId))
|
|
243
|
-
return;
|
|
244
|
-
const runtime = this.opts.getRuntime();
|
|
245
|
-
if (runtime.sessions.get(sessionId)?.currentRunId)
|
|
246
|
-
return;
|
|
247
|
-
const queue = readQueue();
|
|
248
|
-
const pending = queue.sessions[sessionId] ?? [];
|
|
249
|
-
const next = pending.shift();
|
|
250
|
-
if (!next) {
|
|
251
|
-
this.broadcast(sessionId);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
const dispatchMessage = next.origin === 'external'
|
|
255
|
-
? this.mergeExternalMessages(next, pending)
|
|
256
|
-
: next;
|
|
257
|
-
if (pending.length)
|
|
258
|
-
queue.sessions[sessionId] = pending;
|
|
259
|
-
else
|
|
260
|
-
delete queue.sessions[sessionId];
|
|
261
|
-
writeQueue(queue);
|
|
262
|
-
this.broadcast(sessionId);
|
|
263
|
-
this.draining.add(sessionId);
|
|
264
|
-
try {
|
|
265
|
-
await this.dispatchQueuedMessage(dispatchMessage);
|
|
266
|
-
}
|
|
267
|
-
finally {
|
|
268
|
-
this.draining.delete(sessionId);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
mergeExternalMessages(first, pending) {
|
|
272
|
-
const batch = [first];
|
|
273
|
-
while (pending[0]?.origin === 'external') {
|
|
274
|
-
batch.push(pending.shift());
|
|
275
|
-
}
|
|
276
|
-
if (batch.length === 1)
|
|
277
|
-
return first;
|
|
278
|
-
return {
|
|
279
|
-
...first,
|
|
280
|
-
id: `external-batch-${first.id}`,
|
|
281
|
-
text: batch.map((message, index) => {
|
|
282
|
-
const label = batch.length > 1 ? `外部消息 ${index + 1}/${batch.length}` : '外部消息';
|
|
283
|
-
return `${label}\n${message.text}`;
|
|
284
|
-
}).join('\n\n'),
|
|
285
|
-
attachments: batch.flatMap((message) => message.attachments ?? []),
|
|
286
|
-
updatedAt: nowIso(),
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
async dispatchQueuedMessage(message) {
|
|
290
|
-
await this.opts.dispatchReq({
|
|
291
|
-
type: 'req',
|
|
292
|
-
id: `queue-send-${message.id}-${Date.now()}`,
|
|
293
|
-
method: 'chat.send',
|
|
294
|
-
params: {
|
|
295
|
-
sessionId: message.sessionId,
|
|
296
|
-
text: message.text,
|
|
297
|
-
agentType: message.agentType,
|
|
298
|
-
workDir: message.workDir,
|
|
299
|
-
agentSessionId: message.agentSessionId ?? null,
|
|
300
|
-
modelId: message.modelId ?? undefined,
|
|
301
|
-
reasoningEffort: message.reasoningEffort ?? undefined,
|
|
302
|
-
clientMessageId: message.clientMessageId ?? message.id,
|
|
303
|
-
attachments: message.attachments,
|
|
304
|
-
externalChannel: message.externalChannel,
|
|
305
|
-
replyTarget: message.replyTarget,
|
|
306
|
-
waitForDispatch: true,
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
broadcast(sessionId) {
|
|
311
|
-
this.opts.getRuntime().client.sendEvent({
|
|
312
|
-
type: 'event',
|
|
313
|
-
event: 'session.queue.update',
|
|
314
|
-
payload: { queue: this.getSnapshot(sessionId) },
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
}
|
|
1
|
+
import l from"node:fs";import{randomUUID as m}from"node:crypto";import{resolveShennianPath as f}from"../config/index.js";import{mergeProjectedSessions as I}from"./projection.js";import{materializeRemoteChatAttachments as x}from"./remote-attachments.js";const y=f("chat-queue.json");function w(){return{sessions:{}}}function h(){return new Date().toISOString()}function M(n){return`enqueue-send-${n}-${m()}`}function r(){try{const n=JSON.parse(l.readFileSync(y,"utf-8"));return{sessions:n.sessions&&typeof n.sessions=="object"?n.sessions:{}}}catch{return w()}}function u(n){l.mkdirSync(f(""),{recursive:!0}),l.writeFileSync(y,JSON.stringify(n,null,2))}function p(n){if(!Array.isArray(n))return;const e=n.map(s=>{if(!s||typeof s!="object")return null;const t=s,i={path:typeof t.path=="string"?t.path:"",name:typeof t.name=="string"?t.name:"",mimeType:typeof t.mimeType=="string"?t.mimeType:"",...typeof t.previewData=="string"&&t.previewData.trim()?{previewData:t.previewData.trim()}:{}};return i.path&&i.name&&i.mimeType?i:null}).filter(s=>s!=null);return e.length?e:void 0}function R(n){const e=h();return{id:n.queueMessageId||n.clientMessageId||`queue-${m()}`,sessionId:n.sessionId,text:n.text,agentType:n.agentType,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,reasoningEffort:n.reasoningEffort??null,clientMessageId:n.clientMessageId??null,attachments:p(n.attachments),externalChannel:n.externalChannel??null,replyTarget:n.replyTarget??null,origin:n.origin,createdAt:e,updatedAt:e}}class T{opts;draining=new Set;constructor(e){this.opts=e,setTimeout(()=>this.drainIdleQueues(),0).unref?.()}getSnapshot(e){return{sessionId:e,busy:!!this.opts.getRuntime().sessions.get(e)?.currentRunId,pending:r().sessions[e]??[]}}async handleEnqueue(e){const s=this.opts.getRuntime(),t=e.params;if(I(t.sessionListProjection),!t.sessionId||!t.text||!t.agentType||!t.workDir){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, text, agentType and workDir are required"});return}const i=p(t.attachments),a=i?.length?await x({text:t.text,attachments:i,workDir:t.workDir}):{text:t.text,attachments:i,localized:!1},d=!!s.sessions.get(t.sessionId)?.currentRunId;if(!d&&!r().sessions[t.sessionId]?.length){await this.opts.dispatchReq({...e,id:M(e.id),method:"chat.send",params:{...t,text:a.text,responseId:e.id,clientMessageId:t.clientMessageId??t.queueMessageId,waitForDispatch:!0,attachments:a.attachments}});return}const c=r(),g=R({...t,text:a.text,attachments:a.attachments});c.sessions[t.sessionId]=[...c.sessions[t.sessionId]??[],g],u(c),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:!0,queueMessageId:g.id,queue:this.getSnapshot(t.sessionId),...a.localized?{localizedAttachments:!0}:{}}}),d||this.drainNext(t.sessionId)}async handleGet(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId is required"});return}s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}}),this.getSnapshot(t.sessionId).busy||this.drainNext(t.sessionId)}async handleEdit(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId||!t.text){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, queueMessageId and text are required"});return}const i=r(),a=i.sessions[t.sessionId]??[],o=a.findIndex(d=>d.id===t.queueMessageId);if(o<0){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}a[o]={...a[o],text:t.text,attachments:p(t.attachments),updatedAt:h()},i.sessions[t.sessionId]=a,u(i),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleDelete(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId and queueMessageId are required"});return}const i=r(),a=i.sessions[t.sessionId]??[],o=a.filter(d=>d.id!==t.queueMessageId);if(o.length===a.length){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}o.length?i.sessions[t.sessionId]=o:delete i.sessions[t.sessionId],u(i),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}noteTerminal(e){this.drainNext(e)}drainIdleQueues(){const e=r();for(const s of Object.keys(e.sessions))this.getSnapshot(s).busy||this.drainNext(s)}async drainNext(e){if(this.draining.has(e)||this.opts.getRuntime().sessions.get(e)?.currentRunId)return;const t=r(),i=t.sessions[e]??[],a=i.shift();if(!a){this.broadcast(e);return}const o=a.origin==="external"?this.mergeExternalMessages(a,i):a;i.length?t.sessions[e]=i:delete t.sessions[e],u(t),this.broadcast(e),this.draining.add(e);try{await this.dispatchQueuedMessage(o)}finally{this.draining.delete(e)}}mergeExternalMessages(e,s){const t=[e];for(;s[0]?.origin==="external";)t.push(s.shift());return t.length===1?e:{...e,id:`external-batch-${e.id}`,text:t.map((i,a)=>`${t.length>1?`\u5916\u90E8\u6D88\u606F ${a+1}/${t.length}`:"\u5916\u90E8\u6D88\u606F"}
|
|
2
|
+
${i.text}`).join(`
|
|
3
|
+
|
|
4
|
+
`),attachments:t.flatMap(i=>i.attachments??[]),updatedAt:h()}}async dispatchQueuedMessage(e){await this.opts.dispatchReq({type:"req",id:`queue-send-${e.id}-${Date.now()}`,method:"chat.send",params:{sessionId:e.sessionId,text:e.text,agentType:e.agentType,workDir:e.workDir,agentSessionId:e.agentSessionId??null,modelId:e.modelId??void 0,reasoningEffort:e.reasoningEffort??void 0,clientMessageId:e.clientMessageId??e.id,attachments:e.attachments,externalChannel:e.externalChannel,replyTarget:e.replyTarget,waitForDispatch:!0}})}broadcast(e){this.opts.getRuntime().client.sendEvent({type:"event",event:"session.queue.update",payload:{queue:this.getSnapshot(e)}})}}export{T as ChatQueueManager};
|
|
@@ -1,72 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/session-manager.test.ts
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
const MAX_REMOTE_ATTACHMENT_BYTES = Number(process.env.SHENNIAN_REMOTE_ATTACHMENT_MAX_BYTES || 50 * 1024 * 1024);
|
|
7
|
-
function safeFileName(name) {
|
|
8
|
-
const cleaned = path.basename(name || 'attachment')
|
|
9
|
-
.normalize('NFKC')
|
|
10
|
-
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
|
|
11
|
-
.replace(/[\r\n\t]+/g, ' ')
|
|
12
|
-
.replace(/\s+/g, ' ')
|
|
13
|
-
.replace(/_+/g, '_')
|
|
14
|
-
.replace(/^[ ._]+|[ ._]+$/g, '');
|
|
15
|
-
return cleaned || 'attachment';
|
|
16
|
-
}
|
|
17
|
-
function uniquePath(dir, name, hash) {
|
|
18
|
-
const safe = safeFileName(name);
|
|
19
|
-
const ext = path.extname(safe);
|
|
20
|
-
const stem = ext ? safe.slice(0, -ext.length) : safe;
|
|
21
|
-
const candidate = path.join(dir, safe);
|
|
22
|
-
if (!fs.existsSync(candidate))
|
|
23
|
-
return candidate;
|
|
24
|
-
return path.join(dir, `${stem}-${hash.slice(0, 8)}${ext}`);
|
|
25
|
-
}
|
|
26
|
-
function isHttpUrl(value) {
|
|
27
|
-
return /^https?:\/\//i.test(value);
|
|
28
|
-
}
|
|
29
|
-
async function downloadRemoteAttachment(attachment, workDir) {
|
|
30
|
-
if (!isHttpUrl(attachment.path))
|
|
31
|
-
return attachment;
|
|
32
|
-
const response = await fetch(attachment.path);
|
|
33
|
-
if (!response.ok)
|
|
34
|
-
return attachment;
|
|
35
|
-
const contentLength = Number(response.headers.get('content-length') || 0);
|
|
36
|
-
if (contentLength > MAX_REMOTE_ATTACHMENT_BYTES)
|
|
37
|
-
return attachment;
|
|
38
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
39
|
-
if (!buffer.byteLength || buffer.byteLength > MAX_REMOTE_ATTACHMENT_BYTES)
|
|
40
|
-
return attachment;
|
|
41
|
-
const hash = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
42
|
-
const uploadDir = path.join(workDir, '.uploads');
|
|
43
|
-
fs.mkdirSync(uploadDir, { recursive: true });
|
|
44
|
-
const filePath = uniquePath(uploadDir, attachment.name, hash);
|
|
45
|
-
if (!fs.existsSync(filePath))
|
|
46
|
-
fs.writeFileSync(filePath, buffer);
|
|
47
|
-
return {
|
|
48
|
-
...attachment,
|
|
49
|
-
path: filePath,
|
|
50
|
-
mimeType: attachment.mimeType || response.headers.get('content-type') || 'application/octet-stream',
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function replaceAttachmentRefs(text, before, after) {
|
|
54
|
-
if (before.path === after.path || !before.path || !after.path)
|
|
55
|
-
return text;
|
|
56
|
-
return text.split(before.path).join(after.path);
|
|
57
|
-
}
|
|
58
|
-
export async function materializeRemoteChatAttachments(input) {
|
|
59
|
-
if (!input.attachments?.length)
|
|
60
|
-
return { text: input.text, attachments: input.attachments, localized: false };
|
|
61
|
-
const materialized = [];
|
|
62
|
-
let text = input.text;
|
|
63
|
-
let localized = false;
|
|
64
|
-
for (const attachment of input.attachments) {
|
|
65
|
-
const next = await downloadRemoteAttachment(attachment, input.workDir).catch(() => attachment);
|
|
66
|
-
if (next.path !== attachment.path && isHttpUrl(attachment.path))
|
|
67
|
-
localized = true;
|
|
68
|
-
text = replaceAttachmentRefs(text, attachment, next);
|
|
69
|
-
materialized.push(next);
|
|
70
|
-
}
|
|
71
|
-
return { text, attachments: materialized, localized };
|
|
72
|
-
}
|
|
1
|
+
import f from"node:crypto";import i from"node:fs";import o from"node:path";const l=Number(process.env.SHENNIAN_REMOTE_ATTACHMENT_MAX_BYTES||50*1024*1024);function m(t){return o.basename(t||"attachment").normalize("NFKC").replace(/[<>:"/\\|?*\x00-\x1F]/g,"_").replace(/[\r\n\t]+/g," ").replace(/\s+/g," ").replace(/_+/g,"_").replace(/^[ ._]+|[ ._]+$/g,"")||"attachment"}function u(t,a,e){const r=m(a),n=o.extname(r),c=n?r.slice(0,-n.length):r,s=o.join(t,r);return i.existsSync(s)?o.join(t,`${c}-${e.slice(0,8)}${n}`):s}function p(t){return/^https?:\/\//i.test(t)}async function d(t,a){if(!p(t.path))return t;const e=await fetch(t.path);if(!e.ok||Number(e.headers.get("content-length")||0)>l)return t;const n=Buffer.from(await e.arrayBuffer());if(!n.byteLength||n.byteLength>l)return t;const c=f.createHash("sha256").update(n).digest("hex"),s=o.join(a,".uploads");i.mkdirSync(s,{recursive:!0});const h=u(s,t.name,c);return i.existsSync(h)||i.writeFileSync(h,n),{...t,path:h,mimeType:t.mimeType||e.headers.get("content-type")||"application/octet-stream"}}function g(t,a,e){return a.path===e.path||!a.path||!e.path?t:t.split(a.path).join(e.path)}async function _(t){if(!t.attachments?.length)return{text:t.text,attachments:t.attachments,localized:!1};const a=[];let e=t.text,r=!1;for(const n of t.attachments){const c=await d(n,t.workDir).catch(()=>n);c.path!==n.path&&p(n.path)&&(r=!0),e=g(e,n,c),a.push(c)}return{text:e,attachments:a,localized:r}}export{_ as materializeRemoteChatAttachments};
|
|
@@ -1,109 +1,3 @@
|
|
|
1
|
-
import fs from '
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const SESSIONS_DIR = resolveShennianPath('sessions');
|
|
5
|
-
const SESSIONS_INDEX_FILE = resolveShennianPath('sessions-index.json');
|
|
6
|
-
function ensureSessionsDir() {
|
|
7
|
-
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
8
|
-
}
|
|
9
|
-
function sessionFile(sessionId) {
|
|
10
|
-
return path.join(SESSIONS_DIR, `${sessionId}.jsonl`);
|
|
11
|
-
}
|
|
12
|
-
function readSessionIndex() {
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(fs.readFileSync(SESSIONS_INDEX_FILE, 'utf-8'));
|
|
15
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function writeSessionIndex(index) {
|
|
22
|
-
fs.mkdirSync(path.dirname(SESSIONS_INDEX_FILE), { recursive: true });
|
|
23
|
-
fs.writeFileSync(SESSIONS_INDEX_FILE, JSON.stringify(index, null, 2));
|
|
24
|
-
}
|
|
25
|
-
function previewFromEnvelope(envelope) {
|
|
26
|
-
const text = envelope.payload.replace(/\s+/g, ' ').trim();
|
|
27
|
-
if (!text || text.startsWith('{"v":1,"type":"tool'))
|
|
28
|
-
return null;
|
|
29
|
-
return text.length > 120 ? `${text.slice(0, 120)}...` : text;
|
|
30
|
-
}
|
|
31
|
-
export function recordSession(input) {
|
|
32
|
-
try {
|
|
33
|
-
const index = readSessionIndex();
|
|
34
|
-
const existing = index[input.sessionId];
|
|
35
|
-
const now = new Date().toISOString();
|
|
36
|
-
index[input.sessionId] = {
|
|
37
|
-
sessionId: input.sessionId,
|
|
38
|
-
agentType: input.agentType,
|
|
39
|
-
workDir: input.workDir,
|
|
40
|
-
agentSessionId: input.agentSessionId ?? existing?.agentSessionId ?? null,
|
|
41
|
-
modelId: input.modelId ?? existing?.modelId ?? null,
|
|
42
|
-
status: input.status ?? existing?.status ?? 'active',
|
|
43
|
-
createdAt: existing?.createdAt ?? now,
|
|
44
|
-
updatedAt: now,
|
|
45
|
-
lastActivityAt: input.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
46
|
-
lastMessagePreview: input.lastMessagePreview ?? existing?.lastMessagePreview ?? null,
|
|
47
|
-
};
|
|
48
|
-
writeSessionIndex(index);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// Local session indexing is best-effort; relay delivery remains authoritative.
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
export function listSessionRecords() {
|
|
55
|
-
return Object.values(readSessionIndex());
|
|
56
|
-
}
|
|
57
|
-
export function appendMessage(sessionId, envelope) {
|
|
58
|
-
try {
|
|
59
|
-
ensureSessionsDir();
|
|
60
|
-
const line = JSON.stringify(envelope) + '\n';
|
|
61
|
-
fs.appendFileSync(sessionFile(sessionId), line, 'utf-8');
|
|
62
|
-
const index = readSessionIndex();
|
|
63
|
-
const existing = index[sessionId];
|
|
64
|
-
if (existing) {
|
|
65
|
-
const preview = previewFromEnvelope(envelope);
|
|
66
|
-
index[sessionId] = {
|
|
67
|
-
...existing,
|
|
68
|
-
updatedAt: new Date().toISOString(),
|
|
69
|
-
lastActivityAt: new Date(envelope.ts).toISOString(),
|
|
70
|
-
lastMessagePreview: preview ?? existing.lastMessagePreview ?? null,
|
|
71
|
-
};
|
|
72
|
-
writeSessionIndex(index);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// Local transcript persistence is best-effort; relay delivery remains authoritative.
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
export function readMessages(sessionId, opts) {
|
|
80
|
-
const file = sessionFile(sessionId);
|
|
81
|
-
if (!fs.existsSync(file))
|
|
82
|
-
return [];
|
|
83
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
84
|
-
const lines = content.split('\n').filter((l) => l.trim().length > 0);
|
|
85
|
-
let messages = [];
|
|
86
|
-
for (const line of lines) {
|
|
87
|
-
try {
|
|
88
|
-
messages.push(JSON.parse(line));
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// skip malformed lines
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
messages.sort((a, b) => b.ts - a.ts);
|
|
95
|
-
if (opts?.before !== undefined) {
|
|
96
|
-
messages = messages.filter((m) => m.ts < opts.before);
|
|
97
|
-
}
|
|
98
|
-
if (opts?.limit !== undefined && opts.limit > 0) {
|
|
99
|
-
messages = messages.slice(0, opts.limit);
|
|
100
|
-
}
|
|
101
|
-
return messages;
|
|
102
|
-
}
|
|
103
|
-
export function listSessions() {
|
|
104
|
-
ensureSessionsDir();
|
|
105
|
-
return fs
|
|
106
|
-
.readdirSync(SESSIONS_DIR)
|
|
107
|
-
.filter((f) => f.endsWith('.jsonl'))
|
|
108
|
-
.map((f) => f.replace(/\.jsonl$/, ''));
|
|
109
|
-
}
|
|
1
|
+
import r from"node:fs";import S from"node:path";import{resolveShennianPath as f}from"../config/index.js";const a=f("sessions"),l=f("sessions-index.json");function u(){r.mkdirSync(a,{recursive:!0})}function g(e){return S.join(a,`${e}.jsonl`)}function d(){try{const e=JSON.parse(r.readFileSync(l,"utf-8"));return e&&typeof e=="object"?e:{}}catch{return{}}}function y(e){r.mkdirSync(S.dirname(l),{recursive:!0}),r.writeFileSync(l,JSON.stringify(e,null,2))}function I(e){const t=e.payload.replace(/\s+/g," ").trim();return!t||t.startsWith('{"v":1,"type":"tool')?null:t.length>120?`${t.slice(0,120)}...`:t}function x(e){try{const t=d(),s=t[e.sessionId],i=new Date().toISOString();t[e.sessionId]={sessionId:e.sessionId,agentType:e.agentType,workDir:e.workDir,agentSessionId:e.agentSessionId??s?.agentSessionId??null,modelId:e.modelId??s?.modelId??null,status:e.status??s?.status??"active",createdAt:s?.createdAt??i,updatedAt:i,lastActivityAt:e.lastActivityAt??s?.lastActivityAt??i,lastMessagePreview:e.lastMessagePreview??s?.lastMessagePreview??null},y(t)}catch{}}function h(){return Object.values(d())}function A(e,t){try{u();const s=JSON.stringify(t)+`
|
|
2
|
+
`;r.appendFileSync(g(e),s,"utf-8");const i=d(),c=i[e];if(c){const n=I(t);i[e]={...c,updatedAt:new Date().toISOString(),lastActivityAt:new Date(t.ts).toISOString(),lastMessagePreview:n??c.lastMessagePreview??null},y(i)}}catch{}}function O(e,t){const s=g(e);if(!r.existsSync(s))return[];const c=r.readFileSync(s,"utf-8").split(`
|
|
3
|
+
`).filter(o=>o.trim().length>0);let n=[];for(const o of c)try{n.push(JSON.parse(o))}catch{}return n.sort((o,m)=>m.ts-o.ts),t?.before!==void 0&&(n=n.filter(o=>o.ts<t.before)),t?.limit!==void 0&&t.limit>0&&(n=n.slice(0,t.limit)),n}function D(){return u(),r.readdirSync(a).filter(e=>e.endsWith(".jsonl")).map(e=>e.replace(/\.jsonl$/,""))}export{A as appendMessage,h as listSessionRecords,D as listSessions,O as readMessages,x as recordSession};
|
|
@@ -48,6 +48,9 @@ export type ChatQueueService = {
|
|
|
48
48
|
noteTerminal(sessionId: string): void;
|
|
49
49
|
getSnapshot(sessionId: string): import('@shennian/wire').ChatQueueSnapshot;
|
|
50
50
|
};
|
|
51
|
+
export type SessionActivityPublisher = {
|
|
52
|
+
publish(sessionId: string, activity: import('@shennian/wire').SessionActivitySnapshot | null): void;
|
|
53
|
+
};
|
|
51
54
|
export type SessionManagerRuntime = {
|
|
52
55
|
client: CliRelayClient;
|
|
53
56
|
pendingTransfers: Map<string, PendingTransfer>;
|
|
@@ -61,4 +64,5 @@ export type SessionManagerRuntime = {
|
|
|
61
64
|
nativeFusion: NativeSessionFusionService | null;
|
|
62
65
|
managerRuntime: ManagerRuntimeService | null;
|
|
63
66
|
chatQueue: ChatQueueService | null;
|
|
67
|
+
activityPublisher?: SessionActivityPublisher | null;
|
|
64
68
|
};
|