shennian 0.2.72 → 0.2.73
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/src/agents/command-spec.js +19 -12
- package/dist/src/agents/external-channel-instructions.d.ts +3 -1
- package/dist/src/agents/external-channel-instructions.js +36 -14
- package/dist/src/channels/base.d.ts +1 -1
- package/dist/src/channels/runtime.d.ts +2 -1
- package/dist/src/channels/runtime.js +16 -3
- package/dist/src/channels/secret-registry.d.ts +4 -1
- package/dist/src/channels/websocket.d.ts +3 -0
- package/dist/src/channels/websocket.js +39 -2
- package/dist/src/channels/wechat-rpa/macos.d.ts +11 -0
- package/dist/src/channels/wechat-rpa/macos.js +63 -0
- package/dist/src/channels/wechat-rpa/normalizer.d.ts +25 -0
- package/dist/src/channels/wechat-rpa/normalizer.js +55 -0
- package/dist/src/channels/wechat-rpa.d.ts +34 -0
- package/dist/src/channels/wechat-rpa.js +123 -0
- package/dist/src/channels/wecom.d.ts +3 -0
- package/dist/src/channels/wecom.js +43 -1
- package/dist/src/manager/prompt.d.ts +1 -1
- package/dist/src/manager/prompt.js +1 -11
- package/dist/src/manager/runtime.js +3 -1
- package/dist/src/session/archive-zip.d.ts +10 -0
- package/dist/src/session/archive-zip.js +220 -0
- package/dist/src/session/handlers/agent-config.js +85 -6
- package/dist/src/session/handlers/chat.js +27 -1
- package/dist/src/session/handlers/fs.d.ts +1 -0
- package/dist/src/session/handlers/fs.js +57 -1
- package/dist/src/session/manager.js +4 -1
- package/package.json +1 -1
|
@@ -111,6 +111,8 @@ export class WeComChannelAdapter {
|
|
|
111
111
|
lastChatReqIds: new Map(),
|
|
112
112
|
dedup: new Set(),
|
|
113
113
|
dedupQueue: [],
|
|
114
|
+
seenContextLineKeys: new Set(),
|
|
115
|
+
seenContextLineQueue: [],
|
|
114
116
|
};
|
|
115
117
|
this.connections.set(config.id, conn);
|
|
116
118
|
}
|
|
@@ -229,7 +231,7 @@ export class WeComChannelAdapter {
|
|
|
229
231
|
return;
|
|
230
232
|
if (reqId)
|
|
231
233
|
conn.lastChatReqIds.set(conversationId, reqId);
|
|
232
|
-
const text = this.extractText(body).trim();
|
|
234
|
+
const text = this.filterContextText(conn, conversationId, this.extractText(body).trim());
|
|
233
235
|
if (!text)
|
|
234
236
|
return;
|
|
235
237
|
this.onMessage?.({
|
|
@@ -282,6 +284,46 @@ export class WeComChannelAdapter {
|
|
|
282
284
|
return String(this.asRecord(body.appmsg)?.title || '').trim();
|
|
283
285
|
return '';
|
|
284
286
|
}
|
|
287
|
+
filterContextText(conn, conversationId, text) {
|
|
288
|
+
if (!text.includes('外部企业微信群消息上下文'))
|
|
289
|
+
return text;
|
|
290
|
+
const lines = text.split(/\r?\n/);
|
|
291
|
+
const headerLines = [];
|
|
292
|
+
const newMessageLines = [];
|
|
293
|
+
let sawContextLine = false;
|
|
294
|
+
for (const line of lines) {
|
|
295
|
+
const normalized = this.normalizeContextLine(line);
|
|
296
|
+
if (!normalized) {
|
|
297
|
+
if (!sawContextLine)
|
|
298
|
+
headerLines.push(line);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
sawContextLine = true;
|
|
302
|
+
const key = `${conversationId}\n${normalized}`;
|
|
303
|
+
if (conn.seenContextLineKeys.has(key))
|
|
304
|
+
continue;
|
|
305
|
+
this.rememberContextLine(conn, key);
|
|
306
|
+
newMessageLines.push(line);
|
|
307
|
+
}
|
|
308
|
+
if (!sawContextLine)
|
|
309
|
+
return text;
|
|
310
|
+
if (!newMessageLines.length)
|
|
311
|
+
return '';
|
|
312
|
+
return [...headerLines, ...newMessageLines].filter((line) => line.trim()).join('\n').trim();
|
|
313
|
+
}
|
|
314
|
+
normalizeContextLine(line) {
|
|
315
|
+
const trimmed = line.trim().replace(/^=>\s*/, '');
|
|
316
|
+
return /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+.+/.test(trimmed) ? trimmed : null;
|
|
317
|
+
}
|
|
318
|
+
rememberContextLine(conn, key) {
|
|
319
|
+
conn.seenContextLineKeys.add(key);
|
|
320
|
+
conn.seenContextLineQueue.push(key);
|
|
321
|
+
while (conn.seenContextLineQueue.length > 2_000) {
|
|
322
|
+
const removed = conn.seenContextLineQueue.shift();
|
|
323
|
+
if (removed)
|
|
324
|
+
conn.seenContextLineKeys.delete(removed);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
285
327
|
raiseForWeComError(payload, operation) {
|
|
286
328
|
const errcode = typeof payload.errcode === 'number' ? payload.errcode : Number(payload.errcode || 0);
|
|
287
329
|
if (!Number.isFinite(errcode) || errcode === 0)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const MANAGER_SYSTEM_PROMPT = "\u4F60\u662F\u9879\u76EE\u7ECF\u7406\uFF0C\u662F\u5F53\u524D\u9879\u76EE\u7684\u7BA1\u7406\u8005\u3002\n\n\u4F60\u7684\u804C\u8D23\uFF1A\n- \u7406\u89E3\u7528\u6237\u76EE\u6807\u3002\n- \u62C6\u89E3\u4EFB\u52A1\u3002\n- \u521B\u5EFA\u3001\u6307\u6D3E\u3001\u89C2\u5BDF\u548C\u505C\u6B62\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u4E0B\u7684 worker Agent session\u3002\n- \u6C47\u603B worker \u7ED3\u679C\u3002\n- \u5224\u65AD\u662F\u5426\u9700\u8981\u7EE7\u7EED\u7B49\u5F85\u3001\u8C03\u6574\u5B89\u6392\u3001\u8BE2\u95EE\u7528\u6237\u6216\u9A8C\u6536\u3002\n- \u5728\u9879\u76EE .shennian/ \u76EE\u5F55\u4E0B\u7EF4\u62A4\u5FC5\u8981\u7684\u8BA1\u5212\u3001\u8BB0\u5F55\u548C\u9879\u76EE\u8BB0\u5FC6\u3002\n\n\u4F60\u7684\u8FB9\u754C\uFF1A\n- \u4E0D\u8981\u628A\u81EA\u5DF1\u5F53\u4F5C\u4E3B\u8981\u6267\u884C\u8005\u3002\n- \u4E0D\u8981\u76F4\u63A5\u7F16\u8F91\u4E1A\u52A1\u4EE3\u7801\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u4EB2\u81EA\u6267\u884C\u3002\n- \u53EF\u4EE5\u8BFB\u53D6\u6587\u4EF6\u3001\u641C\u7D22\u9879\u76EE\u548C\u68C0\u67E5\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u4FBF\u505A\u5224\u65AD\u3002\n- \u9700\u8981\u4FEE\u6539\u4EE3\u7801\u3001\u8FD0\u884C\u6D4B\u8BD5\u3001\u8C03\u7814\u65B9\u6848\u65F6\uFF0C\u4F18\u5148\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u6BCF\u6B21\u6536\u5230\u65B0\u4EFB\u52A1\u3001\u8865\u5145\u8981\u6C42\u3001\u7EA0\u504F\u6216\u5916\u90E8\u6D88\u606F\u65F6\uFF0C\u5148\u7528 sessions list \u67E5\u770B\u5F53\u524D\u540C\u9879\u76EE worker\uFF1B\u5982\u679C\u53EF\u80FD\u76F8\u5173\uFF0C\u518D\u7528 sessions read \u8BFB\u53D6\u5FC5\u8981\u6458\u8981\u540E\u5224\u65AD\u662F\u5426\u590D\u7528\u3002\n- \u5982\u679C\u5DF2\u6709 worker \u6B63\u5728\u5904\u7406\u540C\u4E00\u76EE\u6807\u3001\u540C\u4E00\u529F\u80FD\u533A\u3001\u540C\u4E00\u6587\u4EF6\u8303\u56F4\u6216\u540C\u4E00\u95EE\u9898\u94FE\u8DEF\uFF0C\u4F18\u5148\u7528 sessions send \u628A\u65B0\u8981\u6C42\u53D1\u7ED9\u8FD9\u4E2A worker\uFF1B\u5373\u4F7F worker \u6B63\u5FD9\u4E5F\u53EF\u4EE5\u53D1\u9001\uFF0C\u9ED8\u8BA4\u4F1A\u8FDB\u5165\u672C\u673A\u961F\u5217\uFF0C\u4E0D\u8981\u56E0\u4E3A\u5B83\u5FD9\u5C31\u65B0\u5EFA worker\u3002\n- \u53EA\u6709\u6CA1\u6709\u76F8\u5173 worker\u3001\u73B0\u6709 worker \u5DF2\u660E\u663E\u4E0D\u9002\u5408\u7EE7\u7EED\u63A8\u8FDB\u3001\u6216\u4EFB\u52A1\u9700\u8981\u5E76\u884C\u62C6\u5206\u7ED9\u4E0D\u540C\u4E13\u957F\u65F6\uFF0C\u624D\u521B\u5EFA\u65B0\u7684 worker\u3002\n- \u521B\u5EFA\u6216\u6307\u6D3E worker \u540E\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u5F53\u573A\u7EE7\u7EED\u8C03\u5EA6\uFF0C\u5426\u5219\u56DE\u590D\u7528\u6237\u5DF2\u5B89\u6392\u5E76\u7ED3\u675F\u5F53\u524D turn\uFF1B\u4E0D\u8981\u4E3B\u52A8\u8F6E\u8BE2 worker \u72B6\u6001\uFF0C\u795E\u5FF5\u4F1A\u5728 worker \u7EC8\u6001\u6216\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u91CD\u65B0\u5524\u9192\u4F60\u3002\n- sessions read \u8FD4\u56DE\u7684\u662F\u7ED9\u7BA1\u7406\u8005\u770B\u7684\u7B80\u6D01\u8FDB\u5C55\u3001\u5DE5\u5177\u6458\u8981\u548C\u6700\u7EC8\u7ED3\u679C\uFF0C\u4E0D\u662F\u539F\u59CB\u6D41\u5F0F token\uFF1B\u4E0D\u8981\u8981\u6C42\u8BFB\u53D6\u6216\u8F6C\u8FF0\u5B8C\u6574\u6D41\u5F0F\u65E5\u5FD7\u3002\n- \u53EA\u80FD\u7BA1\u7406\u4E0E\u4F60\u5904\u4E8E\u540C\u4E00\u53F0\u673A\u5668\u3001\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u7684\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u8DE8\u673A\u5668\u6216\u8DE8\u9879\u76EE\u8C03\u5EA6\u3002\n- \u4E0D\u8981\u65E0\u9650\u5FAA\u73AF\uFF1B\u6CA1\u6709\u660E\u786E\u4E0B\u4E00\u6B65\u65F6\u8BE2\u95EE\u7528\u6237\u6216\u7ED3\u675F\u5F53\u524D turn \u7B49\u5F85\u7CFB\u7EDF\u4E8B\u4EF6\u3002\n- \u4E0D\u8981\u81EA\u5DF1\u8BBE\u7F6E\u5B9A\u65F6\u5524\u9192\uFF1B\u795E\u5FF5\u4F1A\u5728\u7528\u6237\u6D88\u606F\u3001worker \u7EC8\u6001\u6216 worker \u957F\u8FD0\u884C\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u5524\u9192\u4F60\u3002\n- \u5916\u90E8\u6D88\u606F\u901A\u9053\u4E8B\u4EF6\u4F1A\u50CF\u666E\u901A\u7528\u6237\u6D88\u606F\u4E00\u6837\u9001\u8FBE\uFF0C\u683C\u5F0F\u7C7B\u4F3C\u201C\u5916\u90E8\u6D88\u606F / \u53D1\u9001\u4EBA\u201D\u540E\u8DDF\u6D88\u606F\u5185\u5BB9\uFF0C\u53EF\u80FD\u662F\u5408\u5E76\u6D88\u606F\uFF0C\u4E5F\u53EF\u80FD\u5305\u542B\u56FE\u7247\u3001\u89C6\u9891\u6216\u6587\u4EF6 URL\u3002\n- \
|
|
1
|
+
export declare const MANAGER_SYSTEM_PROMPT = "\u4F60\u662F\u9879\u76EE\u7ECF\u7406\uFF0C\u662F\u5F53\u524D\u9879\u76EE\u7684\u7BA1\u7406\u8005\u3002\n\n\u4F60\u7684\u804C\u8D23\uFF1A\n- \u7406\u89E3\u7528\u6237\u76EE\u6807\u3002\n- \u62C6\u89E3\u4EFB\u52A1\u3002\n- \u521B\u5EFA\u3001\u6307\u6D3E\u3001\u89C2\u5BDF\u548C\u505C\u6B62\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u4E0B\u7684 worker Agent session\u3002\n- \u6C47\u603B worker \u7ED3\u679C\u3002\n- \u5224\u65AD\u662F\u5426\u9700\u8981\u7EE7\u7EED\u7B49\u5F85\u3001\u8C03\u6574\u5B89\u6392\u3001\u8BE2\u95EE\u7528\u6237\u6216\u9A8C\u6536\u3002\n- \u5728\u9879\u76EE .shennian/ \u76EE\u5F55\u4E0B\u7EF4\u62A4\u5FC5\u8981\u7684\u8BA1\u5212\u3001\u8BB0\u5F55\u548C\u9879\u76EE\u8BB0\u5FC6\u3002\n\n\u4F60\u7684\u8FB9\u754C\uFF1A\n- \u4E0D\u8981\u628A\u81EA\u5DF1\u5F53\u4F5C\u4E3B\u8981\u6267\u884C\u8005\u3002\n- \u4E0D\u8981\u76F4\u63A5\u7F16\u8F91\u4E1A\u52A1\u4EE3\u7801\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u4EB2\u81EA\u6267\u884C\u3002\n- \u53EF\u4EE5\u8BFB\u53D6\u6587\u4EF6\u3001\u641C\u7D22\u9879\u76EE\u548C\u68C0\u67E5\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u4FBF\u505A\u5224\u65AD\u3002\n- \u9700\u8981\u4FEE\u6539\u4EE3\u7801\u3001\u8FD0\u884C\u6D4B\u8BD5\u3001\u8C03\u7814\u65B9\u6848\u65F6\uFF0C\u4F18\u5148\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u6BCF\u6B21\u6536\u5230\u65B0\u4EFB\u52A1\u3001\u8865\u5145\u8981\u6C42\u3001\u7EA0\u504F\u6216\u5916\u90E8\u6D88\u606F\u65F6\uFF0C\u5148\u7528 sessions list \u67E5\u770B\u5F53\u524D\u540C\u9879\u76EE worker\uFF1B\u5982\u679C\u53EF\u80FD\u76F8\u5173\uFF0C\u518D\u7528 sessions read \u8BFB\u53D6\u5FC5\u8981\u6458\u8981\u540E\u5224\u65AD\u662F\u5426\u590D\u7528\u3002\n- \u5982\u679C\u5DF2\u6709 worker \u6B63\u5728\u5904\u7406\u540C\u4E00\u76EE\u6807\u3001\u540C\u4E00\u529F\u80FD\u533A\u3001\u540C\u4E00\u6587\u4EF6\u8303\u56F4\u6216\u540C\u4E00\u95EE\u9898\u94FE\u8DEF\uFF0C\u4F18\u5148\u7528 sessions send \u628A\u65B0\u8981\u6C42\u53D1\u7ED9\u8FD9\u4E2A worker\uFF1B\u5373\u4F7F worker \u6B63\u5FD9\u4E5F\u53EF\u4EE5\u53D1\u9001\uFF0C\u9ED8\u8BA4\u4F1A\u8FDB\u5165\u672C\u673A\u961F\u5217\uFF0C\u4E0D\u8981\u56E0\u4E3A\u5B83\u5FD9\u5C31\u65B0\u5EFA worker\u3002\n- \u53EA\u6709\u6CA1\u6709\u76F8\u5173 worker\u3001\u73B0\u6709 worker \u5DF2\u660E\u663E\u4E0D\u9002\u5408\u7EE7\u7EED\u63A8\u8FDB\u3001\u6216\u4EFB\u52A1\u9700\u8981\u5E76\u884C\u62C6\u5206\u7ED9\u4E0D\u540C\u4E13\u957F\u65F6\uFF0C\u624D\u521B\u5EFA\u65B0\u7684 worker\u3002\n- \u521B\u5EFA\u6216\u6307\u6D3E worker \u540E\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u5F53\u573A\u7EE7\u7EED\u8C03\u5EA6\uFF0C\u5426\u5219\u56DE\u590D\u7528\u6237\u5DF2\u5B89\u6392\u5E76\u7ED3\u675F\u5F53\u524D turn\uFF1B\u4E0D\u8981\u4E3B\u52A8\u8F6E\u8BE2 worker \u72B6\u6001\uFF0C\u795E\u5FF5\u4F1A\u5728 worker \u7EC8\u6001\u6216\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u91CD\u65B0\u5524\u9192\u4F60\u3002\n- sessions read \u8FD4\u56DE\u7684\u662F\u7ED9\u7BA1\u7406\u8005\u770B\u7684\u7B80\u6D01\u8FDB\u5C55\u3001\u5DE5\u5177\u6458\u8981\u548C\u6700\u7EC8\u7ED3\u679C\uFF0C\u4E0D\u662F\u539F\u59CB\u6D41\u5F0F token\uFF1B\u4E0D\u8981\u8981\u6C42\u8BFB\u53D6\u6216\u8F6C\u8FF0\u5B8C\u6574\u6D41\u5F0F\u65E5\u5FD7\u3002\n- \u53EA\u80FD\u7BA1\u7406\u4E0E\u4F60\u5904\u4E8E\u540C\u4E00\u53F0\u673A\u5668\u3001\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u7684\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u8DE8\u673A\u5668\u6216\u8DE8\u9879\u76EE\u8C03\u5EA6\u3002\n- \u4E0D\u8981\u65E0\u9650\u5FAA\u73AF\uFF1B\u6CA1\u6709\u660E\u786E\u4E0B\u4E00\u6B65\u65F6\u8BE2\u95EE\u7528\u6237\u6216\u7ED3\u675F\u5F53\u524D turn \u7B49\u5F85\u7CFB\u7EDF\u4E8B\u4EF6\u3002\n- \u4E0D\u8981\u81EA\u5DF1\u8BBE\u7F6E\u5B9A\u65F6\u5524\u9192\uFF1B\u795E\u5FF5\u4F1A\u5728\u7528\u6237\u6D88\u606F\u3001worker \u7EC8\u6001\u6216 worker \u957F\u8FD0\u884C\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u5524\u9192\u4F60\u3002\n- \u5916\u90E8\u6D88\u606F\u901A\u9053\u4E8B\u4EF6\u4F1A\u50CF\u666E\u901A\u7528\u6237\u6D88\u606F\u4E00\u6837\u9001\u8FBE\uFF0C\u683C\u5F0F\u7C7B\u4F3C\u201C\u5916\u90E8\u6D88\u606F / \u53D1\u9001\u4EBA\u201D\u540E\u8DDF\u6D88\u606F\u5185\u5BB9\uFF0C\u53EF\u80FD\u662F\u5408\u5E76\u6D88\u606F\uFF0C\u4E5F\u53EF\u80FD\u5305\u542B\u56FE\u7247\u3001\u89C6\u9891\u6216\u6587\u4EF6 URL\u3002\n- \u5916\u90E8\u6D88\u606F\u662F\u5426\u9700\u8981\u56DE\u590D\u3001\u8FFD\u95EE\u3001\u5FFD\u7565\u3001\u8F6C\u4EA4\u5185\u90E8\u8D1F\u8D23\u4EBA\uFF0C\u6216\u521B\u5EFA/\u6307\u6D3E worker\uFF0C\u7531\u4F60\u6839\u636E\u9879\u76EE\u4E0A\u4E0B\u6587\u663E\u5F0F\u5224\u65AD\u3002\n- \u5BF9\u5916\u4F60\u662F\u5F53\u524D\u9879\u76EE\u7684\u9879\u76EE\u7ECF\u7406\uFF0C\u4E0D\u8981\u81EA\u79F0\u795E\u5FF5\u3001Manager Agent \u6216 worker\uFF0C\u4E5F\u4E0D\u8981\u89E3\u91CA\u5185\u90E8\u8C03\u5EA6\u673A\u5236\uFF1B\u53EA\u5728\u9700\u8981\u65F6\u7528\u201C\u6211\u8FD9\u8FB9/\u6211\u4EEC\u8FD9\u8FB9\u201D\u6C9F\u901A\u3002\n- \u4E0D\u8981\u628A\u6240\u6709\u7EC6\u8282\u585E\u8FDB\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1B\u9700\u8981\u957F\u671F\u4FDD\u5B58\u7684\u4FE1\u606F\u5199\u5230\u9879\u76EE .shennian/ \u4E0B\u3002\n\n\u9700\u8981\u7BA1\u7406 worker \u6216\u5916\u90E8\u901A\u9053\u65F6\uFF0C\u4F7F\u7528\u672C\u5730\u547D\u4EE4\uFF1A\n- shennian manager sessions list --json\n- shennian manager sessions start --agent <codex|claude|gemini|cursor|opencode|pi|custom:name> --workdir <path> --message <text>\n- shennian manager sessions send --session-id <id> --message <text>\n- shennian manager sessions send --session-id <id> --message <text> --direct\n- shennian manager sessions queue list --session-id <id> --json\n- shennian manager sessions queue edit --session-id <id> --message-id <queueMessageId> --message <text>\n- shennian manager sessions queue delete --session-id <id> --message-id <queueMessageId>\n- shennian manager sessions stop --session-id <id>\uFF08\u7EC8\u6B62\u6B63\u5728\u8FD0\u884C\u7684 worker\uFF1B\u4E5F\u53EF\u7528 terminate/kill \u522B\u540D\uFF09\n- shennian manager sessions read --session-id <id> --limit 200 --json\n- shennian manager memory path\n\n\u53EF\u521B\u5EFA\u7684 worker Agent \u5305\u62EC Codex\u3001Claude Code\u3001Gemini\u3001Cursor\u3001opencode\u3001Nian\uFF0C\u4EE5\u53CA\u672C\u673A custom agent\uFF08custom:<name>\uFF09\u3002\u9ED8\u8BA4\u7528 sessions send \u6392\u961F\u53D1\u9001 worker \u6D88\u606F\uFF1Aworker \u6B63\u5FD9\u65F6\u6D88\u606F\u4F1A\u5728\u672C\u673A daemon \u961F\u5217\u91CC\u7B49\u5F85\uFF0Cworker \u7A7A\u95F2\u65F6\u81EA\u52A8\u6267\u884C\u3002\u961F\u5217\u91CC\u7684\u672A\u6267\u884C\u6D88\u606F\u53EF\u4EE5 list/edit/delete\uFF1B\u5DF2\u7ECF\u5F00\u59CB\u6267\u884C\u7684\u6D88\u606F\u4E0D\u80FD\u7F16\u8F91\u6216\u5220\u9664\uFF0C\u53EA\u80FD stop \u540E\u91CD\u65B0\u53D1\u9001\u3002\u53EA\u6709\u660E\u786E\u9700\u8981\u6253\u65AD\u987A\u5E8F\u65F6\u624D\u4F7F\u7528 --direct\u3002\n\n\u8FD9\u4E9B\u547D\u4EE4\u5DF2\u7ECF\u7531\u795E\u5FF5\u6CE8\u5165\u5F53\u524D Manager \u8EAB\u4EFD\u548C\u540C\u9879\u76EE\u6743\u9650\u8FB9\u754C\u3002\u4E0D\u8981\u5C1D\u8BD5\u4F2A\u9020 Manager session id\u3002";
|
|
2
2
|
export declare function buildManagerPrompt(userText: string): string;
|
|
@@ -24,14 +24,8 @@ export const MANAGER_SYSTEM_PROMPT = `你是项目经理,是当前项目的管
|
|
|
24
24
|
- 不要无限循环;没有明确下一步时询问用户或结束当前 turn 等待系统事件。
|
|
25
25
|
- 不要自己设置定时唤醒;神念会在用户消息、worker 终态或 worker 长运行健康摘要到来时唤醒你。
|
|
26
26
|
- 外部消息通道事件会像普通用户消息一样送达,格式类似“外部消息 / 发送人”后跟消息内容,可能是合并消息,也可能包含图片、视频或文件 URL。
|
|
27
|
+
- 外部消息是否需要回复、追问、忽略、转交内部负责人,或创建/指派 worker,由你根据项目上下文显式判断。
|
|
27
28
|
- 对外你是当前项目的项目经理,不要自称神念、Manager Agent 或 worker,也不要解释内部调度机制;只在需要时用“我这边/我们这边”沟通。
|
|
28
|
-
- 对外回复必须像真人聊天:短回复一条发完;内容较多时按自然段拆成 2-4 条连续消息,每条只讲一个完整主题。
|
|
29
|
-
- 避免把超过 300-500 字的内容塞进单条消息;不要使用 Markdown、编号列表、项目符号或字面 \\n。
|
|
30
|
-
- 外部消息与当前项目无关时可以忽略;需要较长处理时,先简短回复“收到,我先处理/安排一下”,再创建或指派 worker。
|
|
31
|
-
- 向外部群发文字一律调用 shennian manager external send --text "<消息内容>"
|
|
32
|
-
- 向外部群发图片调用 shennian manager external send-image --path "<图片绝对路径>" --caption "<可选说明>"
|
|
33
|
-
- 向外部群发视频调用 shennian manager external send-video --path "<视频绝对路径>" --caption "<可选说明>"
|
|
34
|
-
- 向外部群发文件调用 shennian manager external send-file --path "<文件绝对路径>" --caption "<可选说明>"
|
|
35
29
|
- 不要把所有细节塞进对话上下文;需要长期保存的信息写到项目 .shennian/ 下。
|
|
36
30
|
|
|
37
31
|
需要管理 worker 或外部通道时,使用本地命令:
|
|
@@ -45,10 +39,6 @@ export const MANAGER_SYSTEM_PROMPT = `你是项目经理,是当前项目的管
|
|
|
45
39
|
- shennian manager sessions stop --session-id <id>(终止正在运行的 worker;也可用 terminate/kill 别名)
|
|
46
40
|
- shennian manager sessions read --session-id <id> --limit 200 --json
|
|
47
41
|
- shennian manager memory path
|
|
48
|
-
- shennian manager external send --text <text>
|
|
49
|
-
- shennian manager external send-image --path <path> --caption <text>
|
|
50
|
-
- shennian manager external send-video --path <path> --caption <text>
|
|
51
|
-
- shennian manager external send-file --path <path> --caption <text>
|
|
52
42
|
|
|
53
43
|
可创建的 worker Agent 包括 Codex、Claude Code、Gemini、Cursor、opencode、Nian,以及本机 custom agent(custom:<name>)。默认用 sessions send 排队发送 worker 消息:worker 正忙时消息会在本机 daemon 队列里等待,worker 空闲时自动执行。队列里的未执行消息可以 list/edit/delete;已经开始执行的消息不能编辑或删除,只能 stop 后重新发送。只有明确需要打断顺序时才使用 --direct。
|
|
54
44
|
|
|
@@ -11,6 +11,7 @@ import { readMessages } from '../session/store.js';
|
|
|
11
11
|
import { ChannelRuntime } from '../channels/runtime.js';
|
|
12
12
|
import { splitExternalReplyText } from '../channels/reply-split.js';
|
|
13
13
|
import { resolveShennianPath } from '../config/index.js';
|
|
14
|
+
import { buildExternalChannelInstructions } from '../agents/external-channel-instructions.js';
|
|
14
15
|
let singleton = null;
|
|
15
16
|
export function setManagerRuntimeService(service) {
|
|
16
17
|
singleton = service;
|
|
@@ -793,7 +794,8 @@ ${message || worker.summary || '(无可见摘要)'}
|
|
|
793
794
|
}
|
|
794
795
|
getManagerExternalChannelSystemPrompt(managerSessionId) {
|
|
795
796
|
return this.channelRuntime
|
|
796
|
-
.
|
|
797
|
+
.listManagerExternalChannels(managerSessionId)
|
|
798
|
+
.map((channel) => buildExternalChannelInstructions(channel, undefined, managerSessionId, 'manager'))
|
|
797
799
|
.join('\n\n')
|
|
798
800
|
.trim();
|
|
799
801
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ZipArchiveOptions = {
|
|
2
|
+
maxFiles?: number;
|
|
3
|
+
maxTotalSize?: number;
|
|
4
|
+
};
|
|
5
|
+
export type ZipArchiveResult = {
|
|
6
|
+
outputPath: string;
|
|
7
|
+
fileCount: number;
|
|
8
|
+
totalSize: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function createZipArchive(sourceDir: string, outputPath: string, options?: ZipArchiveOptions): ZipArchiveResult;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// @arch docs/features/file-management-enhancements.md#文件夹打包下载
|
|
2
|
+
// @test src/__tests__/archive-zip.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const DEFAULT_MAX_FILES = 5000;
|
|
6
|
+
const DEFAULT_MAX_TOTAL_SIZE = 1024 * 1024 * 1024;
|
|
7
|
+
const ZIP_VERSION_NEEDED = 20;
|
|
8
|
+
const ZIP_UTF8_FLAG = 0x0800;
|
|
9
|
+
const ZIP_STORE_METHOD = 0;
|
|
10
|
+
const CHUNK_SIZE = 1024 * 1024;
|
|
11
|
+
const crcTable = new Uint32Array(256);
|
|
12
|
+
for (let index = 0; index < 256; index += 1) {
|
|
13
|
+
let value = index;
|
|
14
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
15
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
16
|
+
}
|
|
17
|
+
crcTable[index] = value >>> 0;
|
|
18
|
+
}
|
|
19
|
+
function updateCrc32(crc, buffer) {
|
|
20
|
+
let next = crc;
|
|
21
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
22
|
+
next = crcTable[(next ^ buffer[index]) & 0xff] ^ (next >>> 8);
|
|
23
|
+
}
|
|
24
|
+
return next >>> 0;
|
|
25
|
+
}
|
|
26
|
+
function dosDateTime(mtimeMs) {
|
|
27
|
+
const date = new Date(mtimeMs || Date.now());
|
|
28
|
+
const year = Math.max(1980, Math.min(2107, date.getFullYear()));
|
|
29
|
+
return {
|
|
30
|
+
date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
|
|
31
|
+
time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function writeUInt32(buffer, value, offset) {
|
|
35
|
+
buffer.writeUInt32LE(value >>> 0, offset);
|
|
36
|
+
}
|
|
37
|
+
function writeLocalHeader(fd, entry) {
|
|
38
|
+
const name = Buffer.from(entry.name, 'utf8');
|
|
39
|
+
const { date, time } = dosDateTime(entry.mtimeMs);
|
|
40
|
+
const header = Buffer.alloc(30);
|
|
41
|
+
writeUInt32(header, 0x04034b50, 0);
|
|
42
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 4);
|
|
43
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 6);
|
|
44
|
+
header.writeUInt16LE(ZIP_STORE_METHOD, 8);
|
|
45
|
+
header.writeUInt16LE(time, 10);
|
|
46
|
+
header.writeUInt16LE(date, 12);
|
|
47
|
+
writeUInt32(header, entry.crc32, 14);
|
|
48
|
+
writeUInt32(header, entry.size, 18);
|
|
49
|
+
writeUInt32(header, entry.size, 22);
|
|
50
|
+
header.writeUInt16LE(name.length, 26);
|
|
51
|
+
header.writeUInt16LE(0, 28);
|
|
52
|
+
fs.writeSync(fd, header);
|
|
53
|
+
fs.writeSync(fd, name);
|
|
54
|
+
}
|
|
55
|
+
function writeCentralDirectoryHeader(fd, entry) {
|
|
56
|
+
const name = Buffer.from(entry.name, 'utf8');
|
|
57
|
+
const { date, time } = dosDateTime(entry.mtimeMs);
|
|
58
|
+
const header = Buffer.alloc(46);
|
|
59
|
+
writeUInt32(header, 0x02014b50, 0);
|
|
60
|
+
header.writeUInt16LE(0x031e, 4);
|
|
61
|
+
header.writeUInt16LE(ZIP_VERSION_NEEDED, 6);
|
|
62
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 8);
|
|
63
|
+
header.writeUInt16LE(ZIP_STORE_METHOD, 10);
|
|
64
|
+
header.writeUInt16LE(time, 12);
|
|
65
|
+
header.writeUInt16LE(date, 14);
|
|
66
|
+
writeUInt32(header, entry.crc32, 16);
|
|
67
|
+
writeUInt32(header, entry.size, 20);
|
|
68
|
+
writeUInt32(header, entry.size, 24);
|
|
69
|
+
header.writeUInt16LE(name.length, 28);
|
|
70
|
+
header.writeUInt16LE(0, 30);
|
|
71
|
+
header.writeUInt16LE(0, 32);
|
|
72
|
+
header.writeUInt16LE(0, 34);
|
|
73
|
+
header.writeUInt16LE(0, 36);
|
|
74
|
+
writeUInt32(header, entry.isDir ? 0x10 : 0, 38);
|
|
75
|
+
writeUInt32(header, entry.offset, 42);
|
|
76
|
+
fs.writeSync(fd, header);
|
|
77
|
+
fs.writeSync(fd, name);
|
|
78
|
+
}
|
|
79
|
+
function writeEndOfCentralDirectory(fd, entryCount, centralSize, centralOffset) {
|
|
80
|
+
const header = Buffer.alloc(22);
|
|
81
|
+
writeUInt32(header, 0x06054b50, 0);
|
|
82
|
+
header.writeUInt16LE(0, 4);
|
|
83
|
+
header.writeUInt16LE(0, 6);
|
|
84
|
+
header.writeUInt16LE(entryCount, 8);
|
|
85
|
+
header.writeUInt16LE(entryCount, 10);
|
|
86
|
+
writeUInt32(header, centralSize, 12);
|
|
87
|
+
writeUInt32(header, centralOffset, 16);
|
|
88
|
+
header.writeUInt16LE(0, 20);
|
|
89
|
+
fs.writeSync(fd, header);
|
|
90
|
+
}
|
|
91
|
+
function normalizeZipName(relativePath, isDir) {
|
|
92
|
+
const normalized = relativePath.split(path.sep).join('/');
|
|
93
|
+
return isDir && !normalized.endsWith('/') ? `${normalized}/` : normalized;
|
|
94
|
+
}
|
|
95
|
+
function collectEntries(sourceDir, options) {
|
|
96
|
+
const sourceStat = fs.statSync(sourceDir);
|
|
97
|
+
const rootName = normalizeZipName(path.basename(sourceDir) || 'folder', true);
|
|
98
|
+
const entries = [{
|
|
99
|
+
name: rootName,
|
|
100
|
+
sourcePath: null,
|
|
101
|
+
crc32: 0,
|
|
102
|
+
size: 0,
|
|
103
|
+
isDir: true,
|
|
104
|
+
mtimeMs: sourceStat.mtimeMs,
|
|
105
|
+
}];
|
|
106
|
+
let fileCount = 0;
|
|
107
|
+
let totalSize = 0;
|
|
108
|
+
const visit = (dirPath) => {
|
|
109
|
+
const raw = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
110
|
+
for (const dirent of raw) {
|
|
111
|
+
const fullPath = path.join(dirPath, dirent.name);
|
|
112
|
+
const relative = path.relative(sourceDir, fullPath);
|
|
113
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
114
|
+
continue;
|
|
115
|
+
const stat = fs.lstatSync(fullPath);
|
|
116
|
+
if (stat.isSymbolicLink())
|
|
117
|
+
continue;
|
|
118
|
+
if (stat.isDirectory()) {
|
|
119
|
+
entries.push({
|
|
120
|
+
name: path.posix.join(rootName, normalizeZipName(relative, true)),
|
|
121
|
+
sourcePath: null,
|
|
122
|
+
crc32: 0,
|
|
123
|
+
size: 0,
|
|
124
|
+
isDir: true,
|
|
125
|
+
mtimeMs: stat.mtimeMs,
|
|
126
|
+
});
|
|
127
|
+
visit(fullPath);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!stat.isFile())
|
|
131
|
+
continue;
|
|
132
|
+
fileCount += 1;
|
|
133
|
+
if (fileCount > options.maxFiles)
|
|
134
|
+
throw new Error(`Too many files: ${fileCount}`);
|
|
135
|
+
totalSize += stat.size;
|
|
136
|
+
if (totalSize > options.maxTotalSize) {
|
|
137
|
+
throw new Error(`Folder too large: ${totalSize} bytes`);
|
|
138
|
+
}
|
|
139
|
+
entries.push({
|
|
140
|
+
name: path.posix.join(rootName, normalizeZipName(relative, false)),
|
|
141
|
+
sourcePath: fullPath,
|
|
142
|
+
crc32: computeFileCrc32(fullPath),
|
|
143
|
+
size: stat.size,
|
|
144
|
+
isDir: false,
|
|
145
|
+
mtimeMs: stat.mtimeMs,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
visit(sourceDir);
|
|
150
|
+
return entries;
|
|
151
|
+
}
|
|
152
|
+
function computeFileCrc32(filePath) {
|
|
153
|
+
const fd = fs.openSync(filePath, 'r');
|
|
154
|
+
const buffer = Buffer.alloc(CHUNK_SIZE);
|
|
155
|
+
let crc = 0xffffffff;
|
|
156
|
+
try {
|
|
157
|
+
while (true) {
|
|
158
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
159
|
+
if (bytesRead <= 0)
|
|
160
|
+
break;
|
|
161
|
+
crc = updateCrc32(crc, buffer.subarray(0, bytesRead));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
fs.closeSync(fd);
|
|
166
|
+
}
|
|
167
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
168
|
+
}
|
|
169
|
+
function writeFileData(fd, filePath) {
|
|
170
|
+
const sourceFd = fs.openSync(filePath, 'r');
|
|
171
|
+
const buffer = Buffer.alloc(CHUNK_SIZE);
|
|
172
|
+
try {
|
|
173
|
+
while (true) {
|
|
174
|
+
const bytesRead = fs.readSync(sourceFd, buffer, 0, buffer.length, null);
|
|
175
|
+
if (bytesRead <= 0)
|
|
176
|
+
break;
|
|
177
|
+
fs.writeSync(fd, buffer.subarray(0, bytesRead));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
fs.closeSync(sourceFd);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function createZipArchive(sourceDir, outputPath, options = {}) {
|
|
185
|
+
const sourceStat = fs.statSync(sourceDir);
|
|
186
|
+
if (!sourceStat.isDirectory())
|
|
187
|
+
throw new Error('Not a directory');
|
|
188
|
+
const limits = {
|
|
189
|
+
maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
|
|
190
|
+
maxTotalSize: options.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE,
|
|
191
|
+
};
|
|
192
|
+
const scanned = collectEntries(sourceDir, limits);
|
|
193
|
+
const zipEntries = [];
|
|
194
|
+
let fileCount = 0;
|
|
195
|
+
let totalSize = 0;
|
|
196
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
197
|
+
const fd = fs.openSync(outputPath, 'w');
|
|
198
|
+
try {
|
|
199
|
+
for (const entry of scanned) {
|
|
200
|
+
const offset = fs.fstatSync(fd).size;
|
|
201
|
+
const nextEntry = { ...entry, offset };
|
|
202
|
+
writeLocalHeader(fd, nextEntry);
|
|
203
|
+
if (nextEntry.sourcePath) {
|
|
204
|
+
writeFileData(fd, nextEntry.sourcePath);
|
|
205
|
+
fileCount += 1;
|
|
206
|
+
totalSize += nextEntry.size;
|
|
207
|
+
}
|
|
208
|
+
zipEntries.push(nextEntry);
|
|
209
|
+
}
|
|
210
|
+
const centralOffset = fs.fstatSync(fd).size;
|
|
211
|
+
for (const entry of zipEntries)
|
|
212
|
+
writeCentralDirectoryHeader(fd, entry);
|
|
213
|
+
const centralSize = fs.fstatSync(fd).size - centralOffset;
|
|
214
|
+
writeEndOfCentralDirectory(fd, zipEntries.length, centralSize, centralOffset);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
fs.closeSync(fd);
|
|
218
|
+
}
|
|
219
|
+
return { outputPath, fileCount, totalSize };
|
|
220
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// @arch docs/features/agent-provider-config.md
|
|
2
2
|
// @test src/__tests__/agent-config-status.test.ts
|
|
3
|
+
// @test src/__tests__/agent-config-handler.test.ts
|
|
4
|
+
import { mkdir } from 'node:fs/promises';
|
|
5
|
+
import { createAgent } from '../../agents/adapter.js';
|
|
3
6
|
import { buildManagedAgentEnv, deleteManagedAgentProviderConfig, getAgentConfigSummary, upsertManagedAgentProviderConfig, } from '../../agents/config-status.js';
|
|
7
|
+
import { resolveShennianPath } from '../../config/index.js';
|
|
4
8
|
import { handleAgentsRefresh } from './agents.js';
|
|
9
|
+
const AGENT_TEST_TIMEOUT_MS = 30_000;
|
|
10
|
+
const AGENT_TEST_PROMPT = '这是一次神念连接测试。请只回复 OK,不要解释。';
|
|
11
|
+
const AGENT_TEST_WORK_DIR = resolveShennianPath('tmp', 'agent-config-test');
|
|
5
12
|
function normalizeAgent(value) {
|
|
6
13
|
if (value === 'codex' || value === 'claude' || value === 'pi')
|
|
7
14
|
return value;
|
|
@@ -45,13 +52,85 @@ export async function handleAgentConfigClear(runtime, req) {
|
|
|
45
52
|
export async function handleAgentConfigTest(runtime, req) {
|
|
46
53
|
const agent = normalizeAgent(req.params.agent);
|
|
47
54
|
const summary = getAgentConfigSummary(agent);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
try {
|
|
56
|
+
const result = await runAgentConfigTest(agent);
|
|
57
|
+
runtime.client.sendRes({
|
|
58
|
+
type: 'res',
|
|
59
|
+
id: req.id,
|
|
60
|
+
ok: true,
|
|
61
|
+
payload: { agent, config: getAgentConfigSummary(agent), test: result },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
runtime.client.sendRes({
|
|
66
|
+
type: 'res',
|
|
67
|
+
id: req.id,
|
|
68
|
+
ok: false,
|
|
69
|
+
payload: { agent, config: summary },
|
|
70
|
+
error: err instanceof Error ? err.message : String(err),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function runAgentConfigTest(agent) {
|
|
75
|
+
const adapter = createAgent(agent);
|
|
76
|
+
if (!adapter)
|
|
77
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
78
|
+
const sessionId = `agent-test-${agent}-${Date.now()}`;
|
|
79
|
+
await mkdir(AGENT_TEST_WORK_DIR, { recursive: true });
|
|
80
|
+
let settled = false;
|
|
81
|
+
let failWait = () => { };
|
|
82
|
+
const waitForReply = new Promise((resolve, reject) => {
|
|
83
|
+
const timeout = setTimeout(() => {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
settled = true;
|
|
87
|
+
reject(new Error('Agent test timed out after 30 seconds'));
|
|
88
|
+
}, AGENT_TEST_TIMEOUT_MS);
|
|
89
|
+
function finish(result) {
|
|
90
|
+
if (settled)
|
|
91
|
+
return;
|
|
92
|
+
settled = true;
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
resolve(result);
|
|
95
|
+
}
|
|
96
|
+
function fail(error) {
|
|
97
|
+
if (settled)
|
|
98
|
+
return;
|
|
99
|
+
settled = true;
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
reject(error);
|
|
102
|
+
}
|
|
103
|
+
failWait = fail;
|
|
104
|
+
adapter.on('agentEvent', (event) => {
|
|
105
|
+
if (event.state === 'delta' && event.text && !event.thinking) {
|
|
106
|
+
finish({ mode: 'agent-run', reply: event.text.slice(0, 120) });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (event.state === 'final') {
|
|
110
|
+
finish({ mode: 'agent-run' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (event.state === 'error') {
|
|
114
|
+
fail(new Error(event.message || 'Agent test failed'));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
adapter.on('error', fail);
|
|
54
118
|
});
|
|
119
|
+
try {
|
|
120
|
+
adapter.configure?.({
|
|
121
|
+
sessionId,
|
|
122
|
+
env: getManagedEnvForAgent(agent),
|
|
123
|
+
});
|
|
124
|
+
await adapter.start(sessionId, AGENT_TEST_WORK_DIR, null);
|
|
125
|
+
const sendPromise = adapter.send(AGENT_TEST_PROMPT).catch((err) => {
|
|
126
|
+
failWait(err instanceof Error ? err : new Error(String(err)));
|
|
127
|
+
});
|
|
128
|
+
return await Promise.race([waitForReply, sendPromise.then(() => waitForReply)]);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
adapter.removeAllListeners();
|
|
132
|
+
await adapter.stop().catch(() => { });
|
|
133
|
+
}
|
|
55
134
|
}
|
|
56
135
|
async function broadcastAgents(runtime) {
|
|
57
136
|
const req = {
|
|
@@ -34,6 +34,16 @@ function extractSummary(text) {
|
|
|
34
34
|
const end = newline > 0 ? Math.min(newline, 80) : Math.min(text.length, 80);
|
|
35
35
|
return text.slice(0, end);
|
|
36
36
|
}
|
|
37
|
+
function formatAgentSendFailure(agentType, err) {
|
|
38
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
39
|
+
if (agentType === 'pi' &&
|
|
40
|
+
(raw.includes('429') || raw.includes('daily_quota_exceeded') || raw.includes('nian_quota_exceeded'))) {
|
|
41
|
+
return raw.includes('too quickly') || raw.includes('per minute')
|
|
42
|
+
? 'Nian 请求过于频繁,请稍后再试。'
|
|
43
|
+
: 'Nian 今日额度已用完,次日自动恢复。';
|
|
44
|
+
}
|
|
45
|
+
return `Agent send failed: ${raw}`;
|
|
46
|
+
}
|
|
37
47
|
function getNativeSourceAgentType(agentType, modelId) {
|
|
38
48
|
if (agentType !== 'manager')
|
|
39
49
|
return agentType;
|
|
@@ -500,7 +510,7 @@ export async function handleChatSend(runtime, req) {
|
|
|
500
510
|
});
|
|
501
511
|
};
|
|
502
512
|
const handleSendFailure = async (err, respondToReq) => {
|
|
503
|
-
const message =
|
|
513
|
+
const message = formatAgentSendFailure(requestedAgentType, err);
|
|
504
514
|
console.error(`[chat.send] send failed reqId=${req.id} sessionId=${sessionId} agentType=${agentType} workDir=${resolvedWorkDir} agentSessionId=${session.agentSessionId ?? incomingAgentSid ?? ''}: ${message}`);
|
|
505
515
|
runtime.sessions.delete(sessionId);
|
|
506
516
|
try {
|
|
@@ -518,6 +528,22 @@ export async function handleChatSend(runtime, req) {
|
|
|
518
528
|
seq: 0,
|
|
519
529
|
},
|
|
520
530
|
});
|
|
531
|
+
if (!respondToReq) {
|
|
532
|
+
const errorEnvelope = {
|
|
533
|
+
id: `agent-error-${req.id}-${Date.now()}`,
|
|
534
|
+
sessionId,
|
|
535
|
+
role: 'agent',
|
|
536
|
+
ts: Date.now(),
|
|
537
|
+
payload: message,
|
|
538
|
+
};
|
|
539
|
+
appendMessage(sessionId, errorEnvelope);
|
|
540
|
+
sendSessionMessageEvent(runtime, errorEnvelope, {
|
|
541
|
+
agentType: requestedAgentType,
|
|
542
|
+
workDir: displayWorkDir,
|
|
543
|
+
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
544
|
+
modelId,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
521
547
|
if (respondToReq) {
|
|
522
548
|
runtime.processedReqIds.delete(req.id);
|
|
523
549
|
runtime.client.sendRes({
|
|
@@ -5,6 +5,7 @@ export declare function handleFsRead(runtime: SessionManagerRuntime, req: ReqFra
|
|
|
5
5
|
export declare function handleFsWrite(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
6
6
|
export declare function handleFsRename(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
7
7
|
export declare function handleFsExportMarkdownPdf(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
8
|
+
export declare function handleFsArchiveZip(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
8
9
|
export declare function handleFsTransfer(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
9
10
|
export declare function handleFsTransferStart(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
10
11
|
export declare function handleFsTransferChunk(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
@@ -4,9 +4,12 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { convertMarkdownToPdf, defaultPdfOutputPath, MarkdownPdfBrowserMissingError, } from '../../tools/markdown-to-pdf.js';
|
|
7
|
+
import { createZipArchive } from '../archive-zip.js';
|
|
7
8
|
const FILE_SYSTEM_ROOTS_PATH = '__roots__';
|
|
8
9
|
const MAX_FOLDER_UPLOAD_FILES = 2000;
|
|
9
10
|
const MAX_FOLDER_UPLOAD_TOTAL_SIZE = 1024 * 1024 * 1024;
|
|
11
|
+
const MAX_ARCHIVE_FILES = 5000;
|
|
12
|
+
const MAX_ARCHIVE_TOTAL_SIZE = 1024 * 1024 * 1024;
|
|
10
13
|
function isWindowsAbsolutePath(pathValue) {
|
|
11
14
|
return /^[A-Za-z]:([\\/]|$)/.test(pathValue) || /^\\\\[^\\]+\\[^\\]+/.test(pathValue);
|
|
12
15
|
}
|
|
@@ -263,7 +266,6 @@ export async function handleFsWrite(runtime, req) {
|
|
|
263
266
|
export async function handleFsRename(runtime, req) {
|
|
264
267
|
const requestedPath = req.params.path;
|
|
265
268
|
const newName = req.params.newName;
|
|
266
|
-
const rootPath = req.params.rootPath || requestedPath;
|
|
267
269
|
if (!requestedPath || !newName) {
|
|
268
270
|
runtime.client.sendRes({
|
|
269
271
|
type: 'res',
|
|
@@ -277,6 +279,8 @@ export async function handleFsRename(runtime, req) {
|
|
|
277
279
|
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid newName' });
|
|
278
280
|
return;
|
|
279
281
|
}
|
|
282
|
+
const api = pathApiForPath(requestedPath);
|
|
283
|
+
const rootPath = req.params.rootPath || api.dirname(requestedPath);
|
|
280
284
|
const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
|
|
281
285
|
if (!resolved.ok) {
|
|
282
286
|
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
@@ -373,6 +377,58 @@ export async function handleFsExportMarkdownPdf(runtime, req) {
|
|
|
373
377
|
});
|
|
374
378
|
}
|
|
375
379
|
}
|
|
380
|
+
function safeArchiveBaseName(name) {
|
|
381
|
+
const trimmed = name.trim().replace(/[<>:"/\\|?*\u0000-\u001f]+/g, '-').replace(/[. ]+$/g, '');
|
|
382
|
+
return trimmed || 'folder';
|
|
383
|
+
}
|
|
384
|
+
export async function handleFsArchiveZip(runtime, req) {
|
|
385
|
+
const requestedPath = req.params.path;
|
|
386
|
+
const rootPath = req.params.rootPath || requestedPath;
|
|
387
|
+
if (!requestedPath) {
|
|
388
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'path is required' });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
|
|
392
|
+
if (!resolved.ok) {
|
|
393
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const stat = fs.statSync(resolved.path);
|
|
398
|
+
if (!stat.isDirectory()) {
|
|
399
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Not a directory' });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const api = pathApiForPath(resolved.path);
|
|
403
|
+
const baseName = safeArchiveBaseName(api.basename(resolved.path));
|
|
404
|
+
const archiveDir = path.join(os.tmpdir(), 'shennian-archives');
|
|
405
|
+
const outputPath = path.join(archiveDir, `${baseName}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.zip`);
|
|
406
|
+
const result = createZipArchive(resolved.path, outputPath, {
|
|
407
|
+
maxFiles: MAX_ARCHIVE_FILES,
|
|
408
|
+
maxTotalSize: MAX_ARCHIVE_TOTAL_SIZE,
|
|
409
|
+
});
|
|
410
|
+
runtime.client.sendRes({
|
|
411
|
+
type: 'res',
|
|
412
|
+
id: req.id,
|
|
413
|
+
ok: true,
|
|
414
|
+
payload: {
|
|
415
|
+
sourcePath: resolved.path,
|
|
416
|
+
outputPath: result.outputPath,
|
|
417
|
+
entry: makeFsEntry(result.outputPath),
|
|
418
|
+
fileCount: result.fileCount,
|
|
419
|
+
totalSize: result.totalSize,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
runtime.client.sendRes({
|
|
425
|
+
type: 'res',
|
|
426
|
+
id: req.id,
|
|
427
|
+
ok: false,
|
|
428
|
+
error: err instanceof Error ? err.message : String(err),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
376
432
|
export async function handleFsTransfer(runtime, req) {
|
|
377
433
|
const { name, targetPath, data, direct } = req.params;
|
|
378
434
|
if (!name || !data) {
|
|
@@ -9,7 +9,7 @@ import { handleAgentConfigClear, handleAgentConfigGet, handleAgentConfigTest, ha
|
|
|
9
9
|
import { handleChatAbort, handleChatSend } from './handlers/chat.js';
|
|
10
10
|
import { handleSessionRefresh } from './handlers/session-refresh.js';
|
|
11
11
|
import { handleSessionTitleSet } from './handlers/title.js';
|
|
12
|
-
import { cleanupPendingTransfers, handleFsLs, handleFsRead, handleFsRename, handleFsWrite, handleFsTransfer, handleFsTransferAbort, handleFsTransferChunk, handleFsTransferFinish, handleFsTransferStart, handleFsExportMarkdownPdf, } from './handlers/fs.js';
|
|
12
|
+
import { cleanupPendingTransfers, handleFsLs, handleFsRead, handleFsRename, handleFsWrite, handleFsTransfer, handleFsTransferAbort, handleFsTransferChunk, handleFsTransferFinish, handleFsTransferStart, handleFsExportMarkdownPdf, handleFsArchiveZip, } from './handlers/fs.js';
|
|
13
13
|
import { handleSkillDoctor, handleSkillInstall, handleSkillList, handleSkillSetup, handleSkillUse, } from './handlers/skills.js';
|
|
14
14
|
import { handleRegionProbe, handleRegionSwitch, handleUpgradeSetPolicy, } from './handlers/control.js';
|
|
15
15
|
import { ManagerRuntimeService, setManagerRuntimeService } from '../manager/runtime.js';
|
|
@@ -124,6 +124,9 @@ export class SessionManager {
|
|
|
124
124
|
case 'fs.export.markdown-pdf':
|
|
125
125
|
await handleFsExportMarkdownPdf(runtime, req);
|
|
126
126
|
break;
|
|
127
|
+
case 'fs.archive.zip':
|
|
128
|
+
await handleFsArchiveZip(runtime, req);
|
|
129
|
+
break;
|
|
127
130
|
case 'fs.rename':
|
|
128
131
|
await handleFsRename(runtime, req);
|
|
129
132
|
break;
|