mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-14 18:00:45 +08:00
feat: IM session list relative time + /session N index binding
Local UX enhancements on top of upstream ce8b9e8:
- harness-client.mjs: add toEpochMs/sessionTimeMs, attach time to sessions
- workspace-command.mjs: /sessionlist relative time, /session N index binding
This commit is contained in:
parent
ce8b9e80e7
commit
3bc642bb82
2 changed files with 82 additions and 5 deletions
|
|
@ -40,6 +40,37 @@ function workspaceFromList(workspacePath, workspaceList) {
|
||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toEpochMs(value) {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return value < 1e12 ? value * 1000 : value;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string' && value) {
|
||||||
|
const parsed = Date.parse(value);
|
||||||
|
if (!Number.isNaN(parsed)) return parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionTimeMs(summary) {
|
||||||
|
if (!summary || typeof summary !== 'object') return null;
|
||||||
|
const candidates = [
|
||||||
|
summary?.header?.lastActivityAt,
|
||||||
|
summary?.header?.updatedAt,
|
||||||
|
summary?.header?.createdAt,
|
||||||
|
summary?.projections?.values?.lastActivityAt,
|
||||||
|
summary?.projections?.values?.updatedAt,
|
||||||
|
summary?.projections?.values?.createdAt,
|
||||||
|
summary?.lastActivityAt,
|
||||||
|
summary?.updatedAt,
|
||||||
|
summary?.createdAt,
|
||||||
|
];
|
||||||
|
for (const value of candidates) {
|
||||||
|
const ms = toEpochMs(value);
|
||||||
|
if (ms !== null) return ms;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
||||||
if (!Array.isArray(sessionList?.items)) {
|
if (!Array.isArray(sessionList?.items)) {
|
||||||
throw new Error('Harness returned an invalid response for session.list');
|
throw new Error('Harness returned an invalid response for session.list');
|
||||||
|
|
@ -54,7 +85,7 @@ function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
||||||
sessions: workspace.sessionIds.map((sessionId) => {
|
sessions: workspace.sessionIds.map((sessionId) => {
|
||||||
const summary = summaries.get(sessionId);
|
const summary = summaries.get(sessionId);
|
||||||
const title = summary?.projections?.values?.title;
|
const title = summary?.projections?.values?.title;
|
||||||
return {
|
const session = {
|
||||||
sessionId,
|
sessionId,
|
||||||
title: typeof title === 'string' ? title : null,
|
title: typeof title === 'string' ? title : null,
|
||||||
archived: archived.has(sessionId),
|
archived: archived.has(sessionId),
|
||||||
|
|
@ -62,6 +93,9 @@ function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
||||||
origin: summary?.origin === 'subagent' ? 'subagent' : null,
|
origin: summary?.origin === 'subagent' ? 'subagent' : null,
|
||||||
summaryAvailable: summary !== undefined,
|
summaryAvailable: summary !== undefined,
|
||||||
};
|
};
|
||||||
|
const time = sessionTimeMs(summary);
|
||||||
|
if (time !== null) session.time = time;
|
||||||
|
return session;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const MAX_COMMAND_MESSAGE_LENGTH = 1_800;
|
||||||
const MAX_SESSION_ID_LENGTH = 256;
|
const MAX_SESSION_ID_LENGTH = 256;
|
||||||
const UNSAFE_DISPLAY_TEXT = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
const UNSAFE_DISPLAY_TEXT = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
||||||
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
||||||
const SESSION_BIND_USAGE = '用法:/session Session ID';
|
const SESSION_BIND_USAGE = '用法:/session Session ID 或 序号(/session N)';
|
||||||
const SESSION_LIST_USAGE = [
|
const SESSION_LIST_USAGE = [
|
||||||
'用法:',
|
'用法:',
|
||||||
'/sessionlist 列出当前工作区会话',
|
'/sessionlist 列出当前工作区会话',
|
||||||
|
|
@ -175,6 +175,25 @@ async function resolveSessionListWorkspace(selector, harness) {
|
||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatSessionRelativeTime(value) {
|
||||||
|
const ms = typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||||
|
if (ms === null) return '';
|
||||||
|
const date = new Date(ms);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
const hm = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
|
const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||||
|
const now = new Date();
|
||||||
|
const dayDiff = Math.round((startOfDay(now) - startOfDay(date)) / 86_400_000);
|
||||||
|
if (dayDiff === 0) return `今天 ${hm}`;
|
||||||
|
if (dayDiff === 1) return `昨天 ${hm}`;
|
||||||
|
if (dayDiff === 2) return `前天 ${hm}`;
|
||||||
|
if (date.getFullYear() === now.getFullYear()) {
|
||||||
|
return `${date.getMonth() + 1}月${date.getDate()}日 ${hm}`;
|
||||||
|
}
|
||||||
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
|
||||||
|
}
|
||||||
|
|
||||||
function sessionListMessage(workspace, sessions) {
|
function sessionListMessage(workspace, sessions) {
|
||||||
const rows = sessions.map((session) => {
|
const rows = sessions.map((session) => {
|
||||||
const sessionId = safeDisplayText(session?.sessionId);
|
const sessionId = safeDisplayText(session?.sessionId);
|
||||||
|
|
@ -182,7 +201,9 @@ function sessionListMessage(workspace, sessions) {
|
||||||
const title = session?.summaryAvailable === false
|
const title = session?.summaryAvailable === false
|
||||||
? '标题暂不可用'
|
? '标题暂不可用'
|
||||||
: safeDisplayText(session?.title) || '暂无标题';
|
: safeDisplayText(session?.title) || '暂无标题';
|
||||||
return `${title}${session?.archived === true ? '(已归档)' : ''}\n ID: ${sessionId}`;
|
const timeText = formatSessionRelativeTime(session?.time);
|
||||||
|
const annotation = `${timeText ? ` · ${timeText}` : ''}${session?.archived === true ? '(已归档)' : ''}`;
|
||||||
|
return `${title}${annotation}\n ID: ${sessionId}`;
|
||||||
});
|
});
|
||||||
if (rows.length === 0) return `工作区:${workspace}\n该工作区暂无会话。`;
|
if (rows.length === 0) return `工作区:${workspace}\n该工作区暂无会话。`;
|
||||||
return [
|
return [
|
||||||
|
|
@ -191,7 +212,7 @@ function sessionListMessage(workspace, sessions) {
|
||||||
'',
|
'',
|
||||||
...rows.map((row, index) => `${index + 1}. ${row}`),
|
...rows.map((row, index) => `${index + 1}. ${row}`),
|
||||||
'',
|
'',
|
||||||
'绑定用法:/session Session ID',
|
'绑定用法:/session Session ID 或 序号(/session N)',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,7 +268,29 @@ function sessionBindErrorMessage(error) {
|
||||||
|
|
||||||
async function runSessionBindCommand(command, harness, conversationKey) {
|
async function runSessionBindCommand(command, harness, conversationKey) {
|
||||||
const match = SESSION_BIND_COMMAND.exec(command);
|
const match = SESSION_BIND_COMMAND.exec(command);
|
||||||
const sessionId = match?.[1];
|
let sessionId = match?.[1];
|
||||||
|
if (typeof sessionId === 'string' && /^\d+$/u.test(sessionId)) {
|
||||||
|
// 序号模式:把 /session N 解析成当前工作区会话列表中的第 N 个会话
|
||||||
|
if (typeof harness?.listWorkspaceSessions !== 'function'
|
||||||
|
|| typeof harness?.currentWorkspace !== 'function') {
|
||||||
|
return commandResult('当前机器人暂不支持按序号绑定,请使用 /session Session ID。');
|
||||||
|
}
|
||||||
|
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
||||||
|
if (selected.error) return commandResult(selected.error);
|
||||||
|
let listed;
|
||||||
|
try {
|
||||||
|
listed = await harness.listWorkspaceSessions(selected.workspace);
|
||||||
|
harness.assertWorkspaceScope?.();
|
||||||
|
} catch {
|
||||||
|
return commandResult('暂时无法获取会话列表,请稍后重试。');
|
||||||
|
}
|
||||||
|
const position = Number(sessionId);
|
||||||
|
if (!Number.isSafeInteger(position) || position < 1
|
||||||
|
|| position > (listed?.sessions?.length ?? 0)) {
|
||||||
|
return commandResult('会话序号不存在,请先执行 /sessionlist 查看序号。');
|
||||||
|
}
|
||||||
|
sessionId = listed.sessions[position - 1].sessionId;
|
||||||
|
}
|
||||||
if (!validSessionId(sessionId)) return commandResult(SESSION_BIND_USAGE);
|
if (!validSessionId(sessionId)) return commandResult(SESSION_BIND_USAGE);
|
||||||
if (typeof harness?.bindWorkspaceSession !== 'function') {
|
if (typeof harness?.bindWorkspaceSession !== 'function') {
|
||||||
return commandResult('当前机器人暂不支持绑定已有会话。');
|
return commandResult('当前机器人暂不支持绑定已有会话。');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue