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.
Files changed (143) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +22 -0
  2. package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
  3. package/dist/bin/shennian.js +1 -1
  4. package/dist/publish-build-manifest.json +548 -0
  5. package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
  6. package/dist/src/agent-env.js +4 -105
  7. package/dist/src/agents/adapter.d.ts +6 -0
  8. package/dist/src/agents/adapter.js +1 -19
  9. package/dist/src/agents/claude.js +8 -305
  10. package/dist/src/agents/codex-control.d.ts +35 -0
  11. package/dist/src/agents/codex-control.js +2 -0
  12. package/dist/src/agents/codex-utils.js +7 -200
  13. package/dist/src/agents/codex.d.ts +8 -0
  14. package/dist/src/agents/codex.js +15 -863
  15. package/dist/src/agents/command-spec.js +2 -413
  16. package/dist/src/agents/config-status.js +1 -226
  17. package/dist/src/agents/cursor.js +1 -249
  18. package/dist/src/agents/custom.js +4 -271
  19. package/dist/src/agents/detect.js +1 -56
  20. package/dist/src/agents/external-channel-instructions.js +10 -94
  21. package/dist/src/agents/gemini.js +1 -173
  22. package/dist/src/agents/manager.js +13 -157
  23. package/dist/src/agents/model-registry/cache.js +1 -37
  24. package/dist/src/agents/model-registry/discovery.js +2 -187
  25. package/dist/src/agents/model-registry/parsers.js +4 -447
  26. package/dist/src/agents/model-registry/runner.js +1 -30
  27. package/dist/src/agents/model-registry/service.js +1 -78
  28. package/dist/src/agents/model-registry/types.js +1 -8
  29. package/dist/src/agents/model-registry.js +1 -18
  30. package/dist/src/agents/openclaw.js +2 -275
  31. package/dist/src/agents/opencode.js +1 -231
  32. package/dist/src/agents/pi-context.js +12 -217
  33. package/dist/src/agents/pi.js +14 -723
  34. package/dist/src/agents/platform-instructions.js +9 -54
  35. package/dist/src/channels/base.d.ts +4 -1
  36. package/dist/src/channels/base.js +1 -3
  37. package/dist/src/channels/registry.js +1 -30
  38. package/dist/src/channels/reply-split.js +10 -89
  39. package/dist/src/channels/runtime.d.ts +1 -0
  40. package/dist/src/channels/runtime.js +5 -533
  41. package/dist/src/channels/secret-registry.d.ts +1 -0
  42. package/dist/src/channels/secret-registry.js +1 -46
  43. package/dist/src/channels/websocket.js +8 -378
  44. package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
  45. package/dist/src/channels/wechat-channel/anchor.js +1 -0
  46. package/dist/src/channels/wechat-channel/client.d.ts +74 -0
  47. package/dist/src/channels/wechat-channel/client.js +1 -0
  48. package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
  49. package/dist/src/channels/wechat-channel/cooldown.js +1 -0
  50. package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
  51. package/dist/src/channels/wechat-channel/fingerprint.js +1 -0
  52. package/dist/src/channels/wechat-channel/helper-assets.d.ts +37 -0
  53. package/dist/src/channels/wechat-channel/helper-assets.js +1 -0
  54. package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
  55. package/dist/src/channels/wechat-channel/helper-client.js +3 -0
  56. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
  57. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -0
  58. package/dist/src/channels/wechat-channel/index.d.ts +17 -0
  59. package/dist/src/channels/wechat-channel/index.js +1 -0
  60. package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
  61. package/dist/src/channels/wechat-channel/ledger.js +1 -0
  62. package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
  63. package/dist/src/channels/wechat-channel/media-resolver.js +1 -0
  64. package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
  65. package/dist/src/channels/wechat-channel/message-key.js +1 -0
  66. package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
  67. package/dist/src/channels/wechat-channel/observer.js +1 -0
  68. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +69 -0
  69. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -0
  70. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  71. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  72. package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
  73. package/dist/src/channels/wechat-channel/preflight.js +1 -0
  74. package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
  75. package/dist/src/channels/wechat-channel/runner.js +1 -0
  76. package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
  77. package/dist/src/channels/wechat-channel/runtime.js +1 -0
  78. package/dist/src/channels/wechat-channel/scheduler.d.ts +35 -0
  79. package/dist/src/channels/wechat-channel/scheduler.js +1 -0
  80. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  81. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  82. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  83. package/dist/src/channels/wechat-rpa.d.ts +21 -0
  84. package/dist/src/channels/wechat-rpa.js +6 -1022
  85. package/dist/src/channels/wecom.js +4 -357
  86. package/dist/src/commands/agent.js +6 -131
  87. package/dist/src/commands/daemon-windows.js +8 -48
  88. package/dist/src/commands/daemon.js +19 -1013
  89. package/dist/src/commands/external-attachments.js +1 -51
  90. package/dist/src/commands/external.js +1 -137
  91. package/dist/src/commands/manager.js +2 -389
  92. package/dist/src/commands/pair-qr.js +1 -6
  93. package/dist/src/commands/pair.js +9 -287
  94. package/dist/src/commands/tools.js +1 -34
  95. package/dist/src/commands/upgrade.js +1 -198
  96. package/dist/src/config/index.js +1 -35
  97. package/dist/src/daemon-log.js +6 -58
  98. package/dist/src/env-path.js +1 -64
  99. package/dist/src/fs/boundary.js +1 -126
  100. package/dist/src/fs/handler.js +1 -130
  101. package/dist/src/fs/security.js +1 -32
  102. package/dist/src/fs/text-decoder.d.ts +10 -0
  103. package/dist/src/fs/text-decoder.js +1 -0
  104. package/dist/src/index.js +2 -404
  105. package/dist/src/log-reporter.js +1 -16
  106. package/dist/src/manager/prompt.js +29 -34
  107. package/dist/src/manager/registry.js +2 -269
  108. package/dist/src/manager/runtime.js +19 -1003
  109. package/dist/src/native-fusion/config.js +1 -5
  110. package/dist/src/native-fusion/opencode-parser.js +3 -123
  111. package/dist/src/native-fusion/parser-common.js +8 -264
  112. package/dist/src/native-fusion/parsers.js +8 -729
  113. package/dist/src/native-fusion/service.d.ts +10 -0
  114. package/dist/src/native-fusion/service.js +2 -198
  115. package/dist/src/native-fusion/state.js +1 -22
  116. package/dist/src/native-fusion/types.js +1 -1
  117. package/dist/src/region.js +1 -88
  118. package/dist/src/relay/client.js +1 -343
  119. package/dist/src/session/archive-zip.js +1 -220
  120. package/dist/src/session/handlers/agent-config.js +1 -150
  121. package/dist/src/session/handlers/agents.js +1 -55
  122. package/dist/src/session/handlers/chat.js +2 -733
  123. package/dist/src/session/handlers/control.js +1 -55
  124. package/dist/src/session/handlers/fs.js +1 -747
  125. package/dist/src/session/handlers/session-refresh.js +1 -35
  126. package/dist/src/session/handlers/skills.js +1 -121
  127. package/dist/src/session/handlers/title.js +1 -60
  128. package/dist/src/session/handlers/tool-detail.d.ts +3 -0
  129. package/dist/src/session/handlers/tool-detail.js +1 -0
  130. package/dist/src/session/manager.d.ts +3 -0
  131. package/dist/src/session/manager.js +1 -261
  132. package/dist/src/session/projection.js +1 -54
  133. package/dist/src/session/queue.js +4 -317
  134. package/dist/src/session/remote-attachments.js +1 -72
  135. package/dist/src/session/store.js +3 -109
  136. package/dist/src/session/types.d.ts +4 -0
  137. package/dist/src/session/types.js +1 -4
  138. package/dist/src/skills/registry.js +15 -148
  139. package/dist/src/skills/setup.js +1 -101
  140. package/dist/src/tools/markdown-to-pdf.js +10 -346
  141. package/dist/src/upgrade/engine.js +3 -347
  142. package/package.json +3 -2
  143. package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
@@ -1,317 +1,4 @@
1
- // @arch docs/features/session-message-queue.md
2
- // @test src/__tests__/session-manager.test.ts
3
- import fs from 'node:fs';
4
- import { randomUUID } from 'node:crypto';
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
- // @arch docs/architecture/app/chat-attachments-preview.md
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 'node:fs';
2
- import path from 'node:path';
3
- import { resolveShennianPath } from '../config/index.js';
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
  };
@@ -1,4 +1 @@
1
- // @arch docs/architecture/cli/daemon.md#会话管理
2
- // @test src/__tests__/session-manager.test.ts
3
- // @test src/__tests__/model-switching.test.ts
4
- export {};
1
+