fix(channels): address PR review — security, bugs, and reliability

- fix: sanitize remote filenames with basename() and isolate uploads
  in UUID subdirs to prevent path traversal and collision (#2-4, #27)
- fix: use crypto.randomInt() for pairing codes instead of Math.random() (#5)
- fix: pass config.sessionScope instead of hardcoded 'user' (#6);
  add per-channel scope overrides via setChannelScope() for startAll (#7)
- fix: removeSession now returns removed session IDs and persists
  when chatId is provided (#8)
- fix: /clear only removes the cleared session from instructedSessions,
  not all sessions (#9)
- fix: DingTalk @mention stripping now removes only the first mention
  instead of all mentions (#10)
- fix: remove dead TELEGRAF_COMMANDS Set and its guard (#13)
- fix: WeChat cursor saved after message processing, not before (#14)
- fix: crash recovery uses time-window counting instead of resettable
  counter to prevent infinite restart loops (#17)
- fix: call channel.disconnect() before exit on crash exhaustion (#18)
This commit is contained in:
tanzhenxin 2026-03-31 00:57:59 +00:00
parent 2ca45b72f5
commit 7bbd5e6471
9 changed files with 152 additions and 74 deletions

View file

@ -15,7 +15,8 @@ export class SessionRouter {
private bridge: AcpBridge;
private defaultCwd: string;
private scope: SessionScope;
private defaultScope: SessionScope;
private channelScopes: Map<string, SessionScope> = new Map();
private persistPath: string | undefined;
constructor(
@ -26,7 +27,7 @@ export class SessionRouter {
) {
this.bridge = bridge;
this.defaultCwd = defaultCwd;
this.scope = scope;
this.defaultScope = scope;
this.persistPath = persistPath;
}
@ -35,13 +36,19 @@ export class SessionRouter {
this.bridge = bridge;
}
/** Set scope override for a specific channel. */
setChannelScope(channelName: string, scope: SessionScope): void {
this.channelScopes.set(channelName, scope);
}
private routingKey(
channelName: string,
senderId: string,
chatId: string,
threadId?: string,
): string {
switch (this.scope) {
const scope = this.channelScopes.get(channelName) || this.defaultScope;
switch (scope) {
case 'thread':
return `${channelName}:${threadId || chatId}`;
case 'single':
@ -90,35 +97,40 @@ export class SessionRouter {
return false;
}
/**
* Remove session(s) for the given sender. Returns the removed session IDs.
*/
removeSession(
channelName: string,
senderId: string,
chatId?: string,
): boolean {
): string[] {
const removedIds: string[] = [];
if (chatId) {
const key = this.routingKey(channelName, senderId, chatId);
return this.deleteByKey(key);
}
// No chatId: remove all sessions for this sender on this channel
let removed = false;
const prefix = `${channelName}:${senderId}`;
for (const k of [...this.toSession.keys()]) {
if (k.startsWith(prefix)) {
this.deleteByKey(k);
removed = true;
const sessionId = this.deleteByKey(key);
if (sessionId) removedIds.push(sessionId);
} else {
// No chatId: remove all sessions for this sender on this channel
const prefix = `${channelName}:${senderId}`;
for (const k of [...this.toSession.keys()]) {
if (k.startsWith(prefix)) {
const sessionId = this.deleteByKey(k);
if (sessionId) removedIds.push(sessionId);
}
}
}
if (removed) this.persist();
return removed;
if (removedIds.length > 0) this.persist();
return removedIds;
}
private deleteByKey(key: string): boolean {
private deleteByKey(key: string): string | null {
const sessionId = this.toSession.get(key);
if (!sessionId) return false;
if (!sessionId) return null;
this.toSession.delete(key);
this.toTarget.delete(sessionId);
this.toCwd.delete(sessionId);
return true;
return sessionId;
}
/** Get all session entries for crash recovery. */