mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 21:30:49 +08:00
feat(feishu): /archived on|off toggle for archived sessions in lists
The session list (and the numeric /watch index) honors a per-bot persisted policy: /archived off hides archived sessions from cards and index resolution, /archived on restores them. Defaults to on.
This commit is contained in:
parent
5055ffe6c8
commit
961dd5503d
4 changed files with 203 additions and 112 deletions
218
lib/index.js
218
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -92,9 +92,12 @@ const HELP_TEXT = [
|
|||
'/watch [Session ID 或序号] 关注会话,任务完成自动推送',
|
||||
'/unwatch [Session ID 或序号] 取消关注',
|
||||
'/watchlist 查看关注列表',
|
||||
'/archived on|off 会话列表是否包含归档会话',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
||||
const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
|
||||
|
||||
/** Safe user-facing text for bind/workspace failures (no raw messages). */
|
||||
function safeErrorText(error) {
|
||||
switch (error?.code) {
|
||||
|
|
@ -642,6 +645,22 @@ export class FeishuHarnessBridge {
|
|||
await this.#showWatchList(key, event.message.chat_id);
|
||||
return;
|
||||
}
|
||||
if (ARCHIVED_COMMAND.test(commandText)) {
|
||||
const match = ARCHIVED_COMMAND.exec(commandText);
|
||||
const value = match[1]?.toLowerCase();
|
||||
if (value !== 'on' && value !== 'off') {
|
||||
await this.#send(event.message.chat_id, '用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)');
|
||||
return;
|
||||
}
|
||||
if (typeof this.#state?.setIncludeArchivedSessions === 'function') {
|
||||
await this.#state.setIncludeArchivedSessions(value === 'on');
|
||||
}
|
||||
await this.#send(
|
||||
event.message.chat_id,
|
||||
value === 'on' ? '已开启:会话列表包含归档会话。' : '已关闭:会话列表隐藏归档会话。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (NUMBER_REPLY.test(commandText)) {
|
||||
const menu = this.#takeMenu(key);
|
||||
if (menu) {
|
||||
|
|
@ -1139,6 +1158,14 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
}
|
||||
|
||||
/** The sessions visible under the bot's archived policy. */
|
||||
#visibleSessions(sessions) {
|
||||
if (this.#state?.includesArchivedSessions?.() === false) {
|
||||
return sessions.filter((session) => session.archived !== true);
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
async #showSessions({ chatId, key }, selector, page = 0) {
|
||||
try {
|
||||
const resolved = await resolveSessionListWorkspace(selector ?? '', this.#harness);
|
||||
|
|
@ -1147,7 +1174,7 @@ export class FeishuHarnessBridge {
|
|||
return;
|
||||
}
|
||||
const listed = await this.#harness.listWorkspaceSessions(resolved.workspace);
|
||||
const sessions = Array.isArray(listed?.sessions) ? listed.sessions : [];
|
||||
const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
|
||||
const workspace = listed?.workspace ?? resolved.workspace;
|
||||
if (sessions.length === 0) {
|
||||
await this.#send(chatId, `工作区:${workspace}\n该工作区暂无会话。`);
|
||||
|
|
@ -1270,7 +1297,7 @@ export class FeishuHarnessBridge {
|
|||
};
|
||||
if (numeric !== null) {
|
||||
if (!currentPath) return { error: '当前机器人没有可用的工作区,无法按序号解析会话。' };
|
||||
const sessions = await listSessions(currentPath);
|
||||
const sessions = this.#visibleSessions(await listSessions(currentPath));
|
||||
const session = sessions[numeric - 1];
|
||||
if (!session?.sessionId) {
|
||||
return { error: `当前工作区只有 ${sessions.length} 个会话。` };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
const EMPTY_STATE = Object.freeze({ version: 1, sessions: {}, seenMessageIds: [], watches: {}, chatTargets: {} });
|
||||
const EMPTY_STATE = Object.freeze({
|
||||
version: 1,
|
||||
sessions: {},
|
||||
seenMessageIds: [],
|
||||
watches: {},
|
||||
chatTargets: {},
|
||||
includeArchivedSessions: true,
|
||||
});
|
||||
|
||||
/** One conversation key may watch at most this many sessions. */
|
||||
export const MAX_WATCHES_PER_KEY = 20;
|
||||
|
|
@ -32,6 +39,9 @@ export class StateStore {
|
|||
seenMessageIds: Array.isArray(parsed.seenMessageIds) ? parsed.seenMessageIds.slice(-1000) : [],
|
||||
watches: parsed.watches && typeof parsed.watches === 'object' ? parsed.watches : {},
|
||||
chatTargets: parsed.chatTargets && typeof parsed.chatTargets === 'object' ? parsed.chatTargets : {},
|
||||
includeArchivedSessions: typeof parsed.includeArchivedSessions === 'boolean'
|
||||
? parsed.includeArchivedSessions
|
||||
: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ENOENT') throw error;
|
||||
|
|
@ -148,6 +158,17 @@ export class StateStore {
|
|||
await this.#persist();
|
||||
}
|
||||
|
||||
// ── Session-list archived policy (per bot) ───────────────────────────────
|
||||
|
||||
includesArchivedSessions() {
|
||||
return this.#state.includeArchivedSessions !== false;
|
||||
}
|
||||
|
||||
async setIncludeArchivedSessions(include) {
|
||||
this.#state.includeArchivedSessions = include !== false;
|
||||
await this.#persist();
|
||||
}
|
||||
|
||||
async #persist() {
|
||||
const snapshot = JSON.stringify(this.#state, null, 2) + '\n';
|
||||
this.#writeQueue = this.#writeQueue.then(async () => {
|
||||
|
|
|
|||
|
|
@ -2543,3 +2543,46 @@ test('reconnect compensation replays missed turn/end and dedups duplicates', asy
|
|||
assert.equal(cards.length, 2, 'exactly one new completion from compensation (t1 deduped)');
|
||||
assert.match(JSON.stringify(cards[1]), /watched-session/);
|
||||
});
|
||||
|
||||
test('/archived off hides archived sessions from the list and watch index', async () => {
|
||||
const { state } = await watchStoreFixture();
|
||||
const work = join(tmpdir(), 'dsh-im-archived-test-work');
|
||||
mkdirSync(work, { recursive: true });
|
||||
const harness = watchHarness({
|
||||
current: work,
|
||||
sessionsByWorkspace: {
|
||||
[work]: [
|
||||
{ sessionId: 'live-session', title: 'Live', archived: false },
|
||||
{ sessionId: 'old-session', title: 'Old', archived: true },
|
||||
],
|
||||
},
|
||||
});
|
||||
const sent = [];
|
||||
const cards = [];
|
||||
const bridge = new FeishuHarnessBridge({
|
||||
client: cardClient(async ({ msgType, content }) => {
|
||||
if (msgType === 'interactive') cards.push(content);
|
||||
}),
|
||||
channel: {},
|
||||
harness,
|
||||
state,
|
||||
status: bridgeStatus(),
|
||||
allowedSenderOpenIds: new Set(['ou_owner']),
|
||||
});
|
||||
bridge._sent = sent;
|
||||
|
||||
await bridge.accept(event('archived-off', '/archived off', { senderOpenId: 'ou_owner' }));
|
||||
await bridge.waitForIdle();
|
||||
assert.equal(state.includesArchivedSessions(), false);
|
||||
|
||||
await bridge.accept(event('sessions-arch', '/sessionlist', { senderOpenId: 'ou_owner' }));
|
||||
await bridge.waitForIdle();
|
||||
const use = useActionsFromCard(cards.at(-1));
|
||||
assert.deepEqual(use, ['live-session'], 'archived session hidden');
|
||||
|
||||
// The numeric watch index must resolve against the filtered list too.
|
||||
await bridge.accept(event('watch-arch', '/watch 1', { senderOpenId: 'ou_owner' }));
|
||||
await bridge.waitForIdle();
|
||||
assert.ok(state.watchEntry('p2p:ou_owner', 'live-session'));
|
||||
assert.equal(state.watchEntry('p2p:ou_owner', 'old-session'), null);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue