shennian 0.2.88 → 0.2.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/wechat-channel/macos/manifest.json +13 -0
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/src/agents/adapter.d.ts +6 -0
- package/dist/src/agents/codex-control.d.ts +35 -0
- package/dist/src/agents/codex-control.js +188 -0
- package/dist/src/agents/codex.d.ts +8 -0
- package/dist/src/agents/codex.js +53 -0
- package/dist/src/channels/base.d.ts +4 -1
- package/dist/src/channels/runtime.d.ts +1 -0
- package/dist/src/channels/runtime.js +32 -1
- package/dist/src/channels/secret-registry.d.ts +1 -0
- package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
- package/dist/src/channels/wechat-channel/anchor.js +65 -0
- package/dist/src/channels/wechat-channel/client.d.ts +74 -0
- package/dist/src/channels/wechat-channel/client.js +96 -0
- package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
- package/dist/src/channels/wechat-channel/cooldown.js +38 -0
- package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
- package/dist/src/channels/wechat-channel/fingerprint.js +71 -0
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +28 -0
- package/dist/src/channels/wechat-channel/helper-assets.js +68 -0
- package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
- package/dist/src/channels/wechat-channel/helper-client.js +149 -0
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
- package/dist/src/channels/wechat-channel/helper-protocol.js +115 -0
- package/dist/src/channels/wechat-channel/index.d.ts +16 -0
- package/dist/src/channels/wechat-channel/index.js +19 -0
- package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
- package/dist/src/channels/wechat-channel/ledger.js +54 -0
- package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
- package/dist/src/channels/wechat-channel/media-resolver.js +181 -0
- package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
- package/dist/src/channels/wechat-channel/message-key.js +105 -0
- package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
- package/dist/src/channels/wechat-channel/observer.js +118 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +66 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +112 -0
- package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
- package/dist/src/channels/wechat-channel/preflight.js +48 -0
- package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
- package/dist/src/channels/wechat-channel/runner.js +84 -0
- package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
- package/dist/src/channels/wechat-channel/runtime.js +66 -0
- package/dist/src/channels/wechat-channel/scheduler.d.ts +30 -0
- package/dist/src/channels/wechat-channel/scheduler.js +152 -0
- package/dist/src/channels/wechat-rpa.d.ts +21 -0
- package/dist/src/channels/wechat-rpa.js +12 -6
- package/dist/src/commands/manager.js +2 -0
- package/dist/src/fs/text-decoder.d.ts +10 -0
- package/dist/src/fs/text-decoder.js +110 -0
- package/dist/src/manager/runtime.js +4 -0
- package/dist/src/native-fusion/service.d.ts +10 -0
- package/dist/src/native-fusion/service.js +27 -0
- package/dist/src/session/handlers/chat.js +18 -0
- package/dist/src/session/handlers/fs.js +39 -3
- package/dist/src/session/handlers/session-refresh.js +12 -0
- package/dist/src/session/handlers/tool-detail.d.ts +3 -0
- package/dist/src/session/handlers/tool-detail.js +218 -0
- package/dist/src/session/manager.d.ts +3 -0
- package/dist/src/session/manager.js +58 -0
- package/dist/src/session/types.d.ts +4 -0
- package/package.json +2 -2
- package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// @arch docs/architecture/data-retention-and-sync-policy.md#tool-结果
|
|
2
|
+
// @test src/__tests__/session-manager.test.ts
|
|
3
|
+
import { readMessages } from '../store.js';
|
|
4
|
+
const DEFAULT_MAX_CHARS = 2_000;
|
|
5
|
+
const MAX_MAX_CHARS = 8_000;
|
|
6
|
+
const SENSITIVE_KEY_RE = /(?:token|api[-_]?key|password|passwd|pwd|secret|authorization|cookie|credential|private[-_]?key)/i;
|
|
7
|
+
const SENSITIVE_ASSIGNMENT_RE = /\b([A-Z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD|PASSWD|PWD|AUTH)[A-Z0-9_]*)=("[^"]*"|'[^']*'|[^\s]+)/gi;
|
|
8
|
+
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
9
|
+
const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9_-]{12,}\b/g;
|
|
10
|
+
function asRecord(value) {
|
|
11
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
12
|
+
? value
|
|
13
|
+
: null;
|
|
14
|
+
}
|
|
15
|
+
function normalizeDetailRef(value) {
|
|
16
|
+
if (!value)
|
|
17
|
+
return null;
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
try {
|
|
20
|
+
return normalizeDetailRef(JSON.parse(value));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const record = asRecord(value);
|
|
27
|
+
if (!record)
|
|
28
|
+
return null;
|
|
29
|
+
const runId = typeof record.runId === 'string' ? record.runId : undefined;
|
|
30
|
+
const sourceSeq = typeof record.sourceSeq === 'number'
|
|
31
|
+
? record.sourceSeq
|
|
32
|
+
: typeof record.seq === 'number'
|
|
33
|
+
? record.seq
|
|
34
|
+
: undefined;
|
|
35
|
+
const toolIndex = typeof record.toolIndex === 'number' ? record.toolIndex : undefined;
|
|
36
|
+
return { ...(runId ? { runId } : {}), ...(sourceSeq != null ? { sourceSeq } : {}), ...(toolIndex != null ? { toolIndex } : {}) };
|
|
37
|
+
}
|
|
38
|
+
function parseRunIdFromMessageId(messageId) {
|
|
39
|
+
const match = /^agent-(.+)-\d+$/.exec(messageId);
|
|
40
|
+
return match?.[1] ?? null;
|
|
41
|
+
}
|
|
42
|
+
function parseSeqFromMessageId(messageId) {
|
|
43
|
+
const match = /^agent-.+-(\d+)$/.exec(messageId);
|
|
44
|
+
return match ? Number(match[1]) : null;
|
|
45
|
+
}
|
|
46
|
+
function clampMaxChars(value) {
|
|
47
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
48
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
49
|
+
return DEFAULT_MAX_CHARS;
|
|
50
|
+
return Math.min(Math.max(200, Math.floor(parsed)), MAX_MAX_CHARS);
|
|
51
|
+
}
|
|
52
|
+
function redactString(value, maxChars) {
|
|
53
|
+
const redacted = value
|
|
54
|
+
.replace(SENSITIVE_ASSIGNMENT_RE, '$1=[REDACTED]')
|
|
55
|
+
.replace(BEARER_RE, 'Bearer [REDACTED]')
|
|
56
|
+
.replace(OPENAI_KEY_RE, 'sk-[REDACTED]');
|
|
57
|
+
return redacted.length > maxChars ? `${redacted.slice(0, maxChars)}…` : redacted;
|
|
58
|
+
}
|
|
59
|
+
function sanitizeValue(value, maxChars, depth = 0) {
|
|
60
|
+
if (value == null)
|
|
61
|
+
return value;
|
|
62
|
+
if (typeof value === 'string')
|
|
63
|
+
return redactString(value, maxChars);
|
|
64
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
65
|
+
return value;
|
|
66
|
+
if (depth >= 4)
|
|
67
|
+
return '[Truncated]';
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.slice(0, 20).map((item) => sanitizeValue(item, maxChars, depth + 1));
|
|
70
|
+
}
|
|
71
|
+
if (typeof value === 'object') {
|
|
72
|
+
const output = {};
|
|
73
|
+
for (const [key, raw] of Object.entries(value).slice(0, 50)) {
|
|
74
|
+
if (SENSITIVE_KEY_RE.test(key)) {
|
|
75
|
+
output[key] = '[REDACTED]';
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
output[key] = sanitizeValue(raw, maxChars, depth + 1);
|
|
79
|
+
}
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
function extractCommand(args, maxChars) {
|
|
85
|
+
const record = asRecord(args);
|
|
86
|
+
const raw = record?.command;
|
|
87
|
+
if (typeof raw === 'string')
|
|
88
|
+
return redactString(raw, maxChars);
|
|
89
|
+
if (Array.isArray(raw)) {
|
|
90
|
+
const rendered = raw.map((item) => String(item)).join(' ');
|
|
91
|
+
return redactString(rendered, maxChars);
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function parseLocalToolCandidates(sessionId) {
|
|
96
|
+
const messages = readMessages(sessionId);
|
|
97
|
+
const candidates = [];
|
|
98
|
+
for (const message of messages) {
|
|
99
|
+
if (message.role !== 'agent' || !message.payload.trim().startsWith('{'))
|
|
100
|
+
continue;
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(message.payload);
|
|
103
|
+
const type = parsed.type;
|
|
104
|
+
if (type !== 'tool' && type !== 'tool_use' && type !== 'tool_result')
|
|
105
|
+
continue;
|
|
106
|
+
const name = typeof parsed.name === 'string' && parsed.name.trim()
|
|
107
|
+
? parsed.name.trim()
|
|
108
|
+
: 'tool';
|
|
109
|
+
candidates.push({
|
|
110
|
+
id: message.id,
|
|
111
|
+
ts: message.ts,
|
|
112
|
+
name,
|
|
113
|
+
args: parsed.args,
|
|
114
|
+
result: parsed.result,
|
|
115
|
+
status: type === 'tool_result' || Object.prototype.hasOwnProperty.call(parsed, 'result')
|
|
116
|
+
? 'completed'
|
|
117
|
+
: parsed.status === 'completed' || parsed.status === 'failed' || parsed.status === 'running'
|
|
118
|
+
? parsed.status
|
|
119
|
+
: 'running',
|
|
120
|
+
detailRef: normalizeDetailRef(parsed.detailRef),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore malformed local transcript lines.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return candidates;
|
|
128
|
+
}
|
|
129
|
+
function scoreCandidate(candidate, req, detailRef) {
|
|
130
|
+
let score = 0;
|
|
131
|
+
if (candidate.id === req.messageId)
|
|
132
|
+
score += 100;
|
|
133
|
+
if (req.name && candidate.name === req.name)
|
|
134
|
+
score += 20;
|
|
135
|
+
const requestRunId = detailRef?.runId ?? req.runId ?? parseRunIdFromMessageId(req.messageId);
|
|
136
|
+
const requestSeq = detailRef?.sourceSeq ?? req.sourceSeq ?? parseSeqFromMessageId(req.messageId) ?? undefined;
|
|
137
|
+
if (requestRunId) {
|
|
138
|
+
const candidateRunId = candidate.detailRef?.runId ?? parseRunIdFromMessageId(candidate.id);
|
|
139
|
+
if (candidateRunId === requestRunId)
|
|
140
|
+
score += 20;
|
|
141
|
+
}
|
|
142
|
+
if (requestSeq != null) {
|
|
143
|
+
const candidateSeq = candidate.detailRef?.sourceSeq ?? parseSeqFromMessageId(candidate.id);
|
|
144
|
+
if (candidateSeq === requestSeq)
|
|
145
|
+
score += 80;
|
|
146
|
+
}
|
|
147
|
+
if (candidate.args != null)
|
|
148
|
+
score += 5;
|
|
149
|
+
return score;
|
|
150
|
+
}
|
|
151
|
+
function findLocalToolDetail(params) {
|
|
152
|
+
const detailRef = normalizeDetailRef(params.detailRef) ?? normalizeDetailRef({
|
|
153
|
+
runId: params.runId,
|
|
154
|
+
sourceSeq: params.sourceSeq,
|
|
155
|
+
toolIndex: params.toolIndex,
|
|
156
|
+
});
|
|
157
|
+
const candidates = parseLocalToolCandidates(params.sessionId);
|
|
158
|
+
if (candidates.length === 0)
|
|
159
|
+
return null;
|
|
160
|
+
const ranked = candidates
|
|
161
|
+
.map((candidate) => ({ candidate, score: scoreCandidate(candidate, params, detailRef) }))
|
|
162
|
+
.filter((item) => item.score > 0)
|
|
163
|
+
.sort((left, right) => right.score - left.score || right.candidate.ts - left.candidate.ts);
|
|
164
|
+
return ranked[0]?.candidate ?? null;
|
|
165
|
+
}
|
|
166
|
+
export async function handleSessionToolDetail(runtime, req) {
|
|
167
|
+
const params = req.params;
|
|
168
|
+
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
|
|
169
|
+
const messageId = typeof params.messageId === 'string' ? params.messageId : '';
|
|
170
|
+
if (!sessionId || !messageId) {
|
|
171
|
+
runtime.client.sendRes({
|
|
172
|
+
type: 'res',
|
|
173
|
+
id: req.id,
|
|
174
|
+
ok: false,
|
|
175
|
+
error: 'sessionId and messageId are required',
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const request = {
|
|
180
|
+
sessionId,
|
|
181
|
+
messageId,
|
|
182
|
+
...(typeof params.name === 'string' ? { name: params.name } : {}),
|
|
183
|
+
...(typeof params.toolIndex === 'number' ? { toolIndex: params.toolIndex } : {}),
|
|
184
|
+
...(typeof params.runId === 'string' ? { runId: params.runId } : {}),
|
|
185
|
+
...(typeof params.sourceSeq === 'number' ? { sourceSeq: params.sourceSeq } : {}),
|
|
186
|
+
...(params.detailRef !== undefined ? { detailRef: params.detailRef } : {}),
|
|
187
|
+
maxChars: clampMaxChars(params.maxChars),
|
|
188
|
+
};
|
|
189
|
+
const maxChars = clampMaxChars(request.maxChars);
|
|
190
|
+
const match = findLocalToolDetail(request);
|
|
191
|
+
if (!match || match.args == null) {
|
|
192
|
+
const payload = {
|
|
193
|
+
detailStatus: 'not_found',
|
|
194
|
+
messageId,
|
|
195
|
+
toolIndex: request.toolIndex ?? 0,
|
|
196
|
+
name: request.name,
|
|
197
|
+
resultOmitted: true,
|
|
198
|
+
source: 'unavailable',
|
|
199
|
+
reason: 'tool detail is not available on this machine',
|
|
200
|
+
};
|
|
201
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const argsSummary = match.args == null ? undefined : sanitizeValue(match.args, maxChars);
|
|
205
|
+
const command = extractCommand(match.args, maxChars);
|
|
206
|
+
const payload = {
|
|
207
|
+
detailStatus: 'available',
|
|
208
|
+
messageId,
|
|
209
|
+
toolIndex: request.toolIndex ?? 0,
|
|
210
|
+
name: match.name,
|
|
211
|
+
status: match.status,
|
|
212
|
+
...(argsSummary !== undefined ? { argsSummary } : {}),
|
|
213
|
+
...(command ? { command } : {}),
|
|
214
|
+
resultOmitted: true,
|
|
215
|
+
source: 'local-session-store',
|
|
216
|
+
};
|
|
217
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload });
|
|
218
|
+
}
|
|
@@ -22,8 +22,11 @@ export declare class SessionManager {
|
|
|
22
22
|
private pendingTransfers;
|
|
23
23
|
private managerRuntime;
|
|
24
24
|
private chatQueue;
|
|
25
|
+
private activityProbeTimer;
|
|
25
26
|
constructor(client: CliRelayClient, nativeFusion?: NativeSessionFusionService | null, cliVersion?: string | undefined);
|
|
26
27
|
private getRuntime;
|
|
28
|
+
private publishSessionActivity;
|
|
29
|
+
private publishManagedActivitySnapshots;
|
|
27
30
|
private reloadCustomAgents;
|
|
28
31
|
handleReq(req: ReqFrame): Promise<void>;
|
|
29
32
|
private evictIdleSessions;
|
|
@@ -8,6 +8,7 @@ import { handleAgentsRefresh, handleModelsRefresh } from './handlers/agents.js';
|
|
|
8
8
|
import { handleAgentConfigClear, handleAgentConfigGet, handleAgentConfigTest, handleAgentConfigUpsert, } from './handlers/agent-config.js';
|
|
9
9
|
import { handleChatAbort, handleChatSend } from './handlers/chat.js';
|
|
10
10
|
import { handleSessionRefresh } from './handlers/session-refresh.js';
|
|
11
|
+
import { handleSessionToolDetail } from './handlers/tool-detail.js';
|
|
11
12
|
import { handleSessionTitleSet } from './handlers/title.js';
|
|
12
13
|
import { cleanupPendingTransfers, handleFsLs, handleFsRead, handleFsRename, handleFsWrite, handleFsTransfer, handleFsTransferAbort, handleFsTransferChunk, handleFsTransferFinish, handleFsTransferStart, handleFsExportMarkdownPdf, handleFsArchiveZip, } from './handlers/fs.js';
|
|
13
14
|
import { handleSkillDoctor, handleSkillInstall, handleSkillList, handleSkillSetup, handleSkillUse, } from './handlers/skills.js';
|
|
@@ -41,6 +42,7 @@ export class SessionManager {
|
|
|
41
42
|
pendingTransfers = new Map();
|
|
42
43
|
managerRuntime;
|
|
43
44
|
chatQueue;
|
|
45
|
+
activityProbeTimer = null;
|
|
44
46
|
constructor(client, nativeFusion = null, cliVersion) {
|
|
45
47
|
this.client = client;
|
|
46
48
|
this.nativeFusion = nativeFusion;
|
|
@@ -56,6 +58,12 @@ export class SessionManager {
|
|
|
56
58
|
setManagerRuntimeService(this.managerRuntime);
|
|
57
59
|
void this.managerRuntime.start();
|
|
58
60
|
this.reloadCustomAgents();
|
|
61
|
+
this.activityProbeTimer = setInterval(() => {
|
|
62
|
+
void this.publishManagedActivitySnapshots().catch((error) => {
|
|
63
|
+
console.error('[session.activity] managed probe failed', error);
|
|
64
|
+
});
|
|
65
|
+
}, 15_000);
|
|
66
|
+
this.activityProbeTimer.unref?.();
|
|
59
67
|
}
|
|
60
68
|
getRuntime() {
|
|
61
69
|
return {
|
|
@@ -71,8 +79,51 @@ export class SessionManager {
|
|
|
71
79
|
nativeFusion: this.nativeFusion,
|
|
72
80
|
managerRuntime: this.managerRuntime,
|
|
73
81
|
chatQueue: this.chatQueue,
|
|
82
|
+
activityPublisher: {
|
|
83
|
+
publish: (sessionId, activity) => this.publishSessionActivity(sessionId, activity),
|
|
84
|
+
},
|
|
74
85
|
};
|
|
75
86
|
}
|
|
87
|
+
publishSessionActivity(sessionId, activity) {
|
|
88
|
+
this.client.sendEvent({
|
|
89
|
+
type: 'event',
|
|
90
|
+
event: 'session.activity',
|
|
91
|
+
payload: { sessionId, activity },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async publishManagedActivitySnapshots() {
|
|
95
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
96
|
+
if (!session.currentRunId || !session.adapter.getStatus)
|
|
97
|
+
continue;
|
|
98
|
+
const status = await session.adapter.getStatus().catch(() => null);
|
|
99
|
+
if (!status?.active || !status.runPhase)
|
|
100
|
+
continue;
|
|
101
|
+
const nowIso = new Date().toISOString();
|
|
102
|
+
this.publishSessionActivity(sessionId, {
|
|
103
|
+
sessionId,
|
|
104
|
+
runId: status.runId || session.currentRunId,
|
|
105
|
+
runPhase: status.runPhase,
|
|
106
|
+
startedAt: new Date(session.lastActiveAt).toISOString(),
|
|
107
|
+
updatedAt: nowIso,
|
|
108
|
+
canStop: status.canStop ?? true,
|
|
109
|
+
});
|
|
110
|
+
const seq = session.heartbeatSeq++;
|
|
111
|
+
this.client.sendAgentEvent({
|
|
112
|
+
type: 'event',
|
|
113
|
+
event: 'agent',
|
|
114
|
+
payload: {
|
|
115
|
+
state: 'heartbeat',
|
|
116
|
+
sessionId,
|
|
117
|
+
runId: status.runId || session.currentRunId,
|
|
118
|
+
seq,
|
|
119
|
+
runPhase: status.runPhase,
|
|
120
|
+
canStop: status.canStop ?? true,
|
|
121
|
+
},
|
|
122
|
+
seq,
|
|
123
|
+
id: `agent-status-${status.runId || session.currentRunId}-${Date.now()}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
76
127
|
reloadCustomAgents() {
|
|
77
128
|
for (const agentType of getRegisteredAgents()) {
|
|
78
129
|
if (agentType.startsWith('custom:')) {
|
|
@@ -109,6 +160,9 @@ export class SessionManager {
|
|
|
109
160
|
case 'session.refresh':
|
|
110
161
|
await handleSessionRefresh(runtime, req);
|
|
111
162
|
break;
|
|
163
|
+
case 'session.tool.detail':
|
|
164
|
+
await handleSessionToolDetail(runtime, req);
|
|
165
|
+
break;
|
|
112
166
|
case 'session.title.set':
|
|
113
167
|
await handleSessionTitleSet(runtime, req);
|
|
114
168
|
break;
|
|
@@ -245,6 +299,10 @@ export class SessionManager {
|
|
|
245
299
|
return resolveAuthorizedPath(p, createAuthorizedFsRoot(rootPath));
|
|
246
300
|
}
|
|
247
301
|
cleanup() {
|
|
302
|
+
if (this.activityProbeTimer) {
|
|
303
|
+
clearInterval(this.activityProbeTimer);
|
|
304
|
+
this.activityProbeTimer = null;
|
|
305
|
+
}
|
|
248
306
|
for (const [, session] of this.sessions) {
|
|
249
307
|
if (session.heartbeatTimer) {
|
|
250
308
|
clearInterval(session.heartbeatTimer);
|
|
@@ -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
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shennian",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.89",
|
|
4
4
|
"description": "Shennian — AI Agent Control Plane CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsc && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
|
|
54
|
-
"build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
|
|
54
|
+
"build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json && node scripts/copy-wechat-rpa-assets.mjs && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\" && node scripts/check-publish-artifact.mjs",
|
|
55
55
|
"dev": "tsc --watch"
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// @arch docs/features/wechat-rpa-channel.md
|
|
2
|
-
// @test packages/cli/src/__tests__/wechat-rpa-download-candidates.test.ts
|
|
3
|
-
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { normalizeReplyText } from './wechat-rpa-confirmation.mjs'
|
|
6
|
-
|
|
7
|
-
export function selectDownloadedAttachment(before, after, startedAt, attachment) {
|
|
8
|
-
const changed = Array.from(after.values())
|
|
9
|
-
.filter((file) => file.mtimeMs >= startedAt - 1_000)
|
|
10
|
-
.filter((file) => isPlausibleDownloadedAttachment(file, attachment))
|
|
11
|
-
.filter((file) => {
|
|
12
|
-
const prev = before.get(file.path)
|
|
13
|
-
return !prev || prev.size !== file.size || prev.mtimeMs !== file.mtimeMs
|
|
14
|
-
})
|
|
15
|
-
if (!changed.length) return null
|
|
16
|
-
const expectedName = normalizeReplyText(attachment?.name || '')
|
|
17
|
-
return changed
|
|
18
|
-
.map((file) => {
|
|
19
|
-
const base = normalizeReplyText(path.basename(file.path))
|
|
20
|
-
const expectedHead = expectedName.slice(0, Math.min(expectedName.length, 16))
|
|
21
|
-
const nameScore = expectedHead && base.includes(expectedHead) ? 10 : 0
|
|
22
|
-
return { ...file, score: nameScore + file.mtimeMs / 1_000_000_000_000 }
|
|
23
|
-
})
|
|
24
|
-
.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs)[0] || null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function selectCachedAttachment(candidates, attachment) {
|
|
28
|
-
const expectedName = normalizeReplyText(attachment?.name || '')
|
|
29
|
-
if (expectedName.length < 4) return null
|
|
30
|
-
const expectedExt = path.extname(String(attachment?.name || '')).toLowerCase()
|
|
31
|
-
const expectedHead = expectedName.slice(0, Math.min(expectedName.length, 24))
|
|
32
|
-
const matched = Array.from(candidates.values())
|
|
33
|
-
.filter((file) => isPlausibleDownloadedAttachment(file, attachment))
|
|
34
|
-
.filter((file) => {
|
|
35
|
-
const base = normalizeReplyText(path.basename(file.path))
|
|
36
|
-
if (base === expectedName) return true
|
|
37
|
-
return expectedHead.length >= 8 && base.includes(expectedHead)
|
|
38
|
-
})
|
|
39
|
-
if (!matched.length) return null
|
|
40
|
-
return matched
|
|
41
|
-
.map((file) => ({
|
|
42
|
-
...file,
|
|
43
|
-
score: (path.extname(file.path).toLowerCase() === expectedExt ? 10 : 0) + file.mtimeMs / 1_000_000_000_000,
|
|
44
|
-
}))
|
|
45
|
-
.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs)[0] || null
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const INTERNAL_EXTENSIONS = new Set([
|
|
49
|
-
'.db',
|
|
50
|
-
'.ini',
|
|
51
|
-
'.log',
|
|
52
|
-
'.plist',
|
|
53
|
-
'.shm',
|
|
54
|
-
'.sqlite',
|
|
55
|
-
'.statistic',
|
|
56
|
-
'.tmp',
|
|
57
|
-
'.wal',
|
|
58
|
-
'.xlog',
|
|
59
|
-
])
|
|
60
|
-
|
|
61
|
-
const ATTACHMENT_EXTENSIONS = new Set([
|
|
62
|
-
'.7z',
|
|
63
|
-
'.aac',
|
|
64
|
-
'.avi',
|
|
65
|
-
'.csv',
|
|
66
|
-
'.doc',
|
|
67
|
-
'.docx',
|
|
68
|
-
'.gif',
|
|
69
|
-
'.heic',
|
|
70
|
-
'.jpeg',
|
|
71
|
-
'.jpg',
|
|
72
|
-
'.key',
|
|
73
|
-
'.m4a',
|
|
74
|
-
'.m4v',
|
|
75
|
-
'.md',
|
|
76
|
-
'.mov',
|
|
77
|
-
'.mp3',
|
|
78
|
-
'.mp4',
|
|
79
|
-
'.numbers',
|
|
80
|
-
'.pages',
|
|
81
|
-
'.pdf',
|
|
82
|
-
'.png',
|
|
83
|
-
'.ppt',
|
|
84
|
-
'.pptx',
|
|
85
|
-
'.rar',
|
|
86
|
-
'.rtf',
|
|
87
|
-
'.txt',
|
|
88
|
-
'.wav',
|
|
89
|
-
'.webp',
|
|
90
|
-
'.xls',
|
|
91
|
-
'.xlsx',
|
|
92
|
-
'.zip',
|
|
93
|
-
])
|
|
94
|
-
|
|
95
|
-
export function isPlausibleDownloadedAttachment(file, attachment) {
|
|
96
|
-
const base = path.basename(file?.path || '')
|
|
97
|
-
if (!base || base.startsWith('.')) return false
|
|
98
|
-
if (!Number.isFinite(file?.size) || file.size <= 0) return false
|
|
99
|
-
const ext = path.extname(base).toLowerCase()
|
|
100
|
-
if (!ext || INTERNAL_EXTENSIONS.has(ext)) return false
|
|
101
|
-
const expectedExt = path.extname(String(attachment?.name || '')).toLowerCase()
|
|
102
|
-
if (expectedExt && ext !== expectedExt) return false
|
|
103
|
-
if (ATTACHMENT_EXTENSIONS.has(ext)) return true
|
|
104
|
-
return Boolean(expectedExt)
|
|
105
|
-
}
|