mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 00:48:10 +00:00
Upgrade openclaw to 6.10 (#1138)
This commit is contained in:
+123
-10
@@ -2733,6 +2733,25 @@ export async function updateSingleAgentModelProvider(
|
||||
* unknown or future config issues, the reactive auto-repair mechanism
|
||||
* (`runOpenClawDoctorRepair`) runs `openclaw doctor --fix` as a fallback.
|
||||
*/
|
||||
const SKILL_WORKSHOP_TOOL_DENY_ENTRY = 'skill_workshop';
|
||||
const SKILL_CREATOR_SKILL_KEY = 'skill-creator';
|
||||
|
||||
function normalizeToolDenyList(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.filter((entry): entry is string => typeof entry === 'string')
|
||||
: [];
|
||||
}
|
||||
|
||||
function ensureToolDenyIncludes(
|
||||
deny: string[],
|
||||
entry: string,
|
||||
): { deny: string[]; modified: boolean } {
|
||||
if (deny.includes(entry)) {
|
||||
return { deny, modified: false };
|
||||
}
|
||||
return { deny: [...deny, entry], modified: true };
|
||||
}
|
||||
|
||||
export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
return withConfigLock(async () => {
|
||||
// Skip sanitization if the config file does not exist yet.
|
||||
@@ -2916,18 +2935,19 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
toolsModified = true;
|
||||
}
|
||||
|
||||
// OpenClaw 6.5 moved Skill Workshop into the core skills surface.
|
||||
// ClawX does not expose that durable-skill proposal flow yet, so keep the
|
||||
// built-in tool denied even under tools.profile="full".
|
||||
const deny = Array.isArray(toolsConfig.deny)
|
||||
? toolsConfig.deny.filter((value): value is string => typeof value === 'string')
|
||||
: [];
|
||||
if (!deny.includes('skill_workshop')) {
|
||||
toolsConfig.deny = [...deny, 'skill_workshop'];
|
||||
// OpenClaw 6.5+ routes durable skill edits through the Skill Workshop tool.
|
||||
// ClawX keeps direct skill-creator authoring instead, so deny the workshop
|
||||
// tool even under tools.profile="full".
|
||||
const denyResult = ensureToolDenyIncludes(
|
||||
normalizeToolDenyList(toolsConfig.deny),
|
||||
SKILL_WORKSHOP_TOOL_DENY_ENTRY,
|
||||
);
|
||||
if (denyResult.modified) {
|
||||
toolsConfig.deny = denyResult.deny;
|
||||
toolsModified = true;
|
||||
console.log('[sanitize] Added "skill_workshop" to tools.deny for ClawX desktop');
|
||||
} else if (!Array.isArray(toolsConfig.deny) || toolsConfig.deny.length !== deny.length) {
|
||||
toolsConfig.deny = deny;
|
||||
} else if (!Array.isArray(toolsConfig.deny) || toolsConfig.deny.length !== denyResult.deny.length) {
|
||||
toolsConfig.deny = denyResult.deny;
|
||||
toolsModified = true;
|
||||
}
|
||||
|
||||
@@ -2951,6 +2971,99 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// ── session.dmScope ─────────────────────────────────────────────
|
||||
// OpenClaw defaults DM session routing to "main" (all channels share
|
||||
// agent:main:main), which makes ClawX sidebar conflate feishu, dingtalk,
|
||||
// and other channel DMs into one entry. Set "per-channel-peer" so each
|
||||
// channel+peer gets its own session key (agent:main:feishu:direct:ou_xxx),
|
||||
// letting the sidebar show them as separate conversations with channel badges.
|
||||
const sessionConfig = (
|
||||
config.session && typeof config.session === 'object' && !Array.isArray(config.session)
|
||||
? { ...(config.session as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
if (sessionConfig.dmScope !== 'per-channel-peer' && sessionConfig.dmScope !== 'per-account-channel-peer') {
|
||||
sessionConfig.dmScope = 'per-channel-peer';
|
||||
config.session = sessionConfig;
|
||||
modified = true;
|
||||
console.log('[sanitize] Set session.dmScope="per-channel-peer" so channel DMs appear as separate sessions in ClawX');
|
||||
}
|
||||
|
||||
// ── Skill Workshop hard-disable (OpenClaw 6.10+) ─────────────────
|
||||
const gateway = (
|
||||
config.gateway && typeof config.gateway === 'object'
|
||||
? { ...(config.gateway as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const gatewayTools = (
|
||||
gateway.tools && typeof gateway.tools === 'object'
|
||||
? { ...(gateway.tools as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const gatewayDenyResult = ensureToolDenyIncludes(
|
||||
normalizeToolDenyList(gatewayTools.deny),
|
||||
SKILL_WORKSHOP_TOOL_DENY_ENTRY,
|
||||
);
|
||||
let gatewayModified = gatewayDenyResult.modified;
|
||||
if (gatewayDenyResult.modified) {
|
||||
gatewayTools.deny = gatewayDenyResult.deny;
|
||||
console.log('[sanitize] Added "skill_workshop" to gateway.tools.deny for ClawX desktop');
|
||||
} else if (!Array.isArray(gatewayTools.deny) || gatewayTools.deny.length !== gatewayDenyResult.deny.length) {
|
||||
gatewayTools.deny = gatewayDenyResult.deny;
|
||||
gatewayModified = true;
|
||||
}
|
||||
if (gatewayModified) {
|
||||
gateway.tools = gatewayTools;
|
||||
config.gateway = gateway;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
let skillsObj = (
|
||||
config.skills && typeof config.skills === 'object' && !Array.isArray(config.skills)
|
||||
? { ...(config.skills as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
let skillsModified = false;
|
||||
|
||||
const workshop = (
|
||||
skillsObj.workshop && typeof skillsObj.workshop === 'object'
|
||||
? { ...(skillsObj.workshop as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const autonomous = (
|
||||
workshop.autonomous && typeof workshop.autonomous === 'object'
|
||||
? { ...(workshop.autonomous as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
if (autonomous.enabled !== false) {
|
||||
autonomous.enabled = false;
|
||||
workshop.autonomous = autonomous;
|
||||
skillsObj.workshop = workshop;
|
||||
skillsModified = true;
|
||||
console.log('[sanitize] Disabled skills.workshop.autonomous for ClawX desktop');
|
||||
}
|
||||
|
||||
const skillEntries = (
|
||||
skillsObj.entries && typeof skillsObj.entries === 'object' && !Array.isArray(skillsObj.entries)
|
||||
? { ...(skillsObj.entries as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, Record<string, unknown>>;
|
||||
const skillCreatorEntry = skillEntries[SKILL_CREATOR_SKILL_KEY] || {};
|
||||
if (skillCreatorEntry.enabled !== true) {
|
||||
skillEntries[SKILL_CREATOR_SKILL_KEY] = {
|
||||
...skillCreatorEntry,
|
||||
enabled: true,
|
||||
};
|
||||
skillsObj.entries = skillEntries;
|
||||
skillsModified = true;
|
||||
console.log('[sanitize] Enabled bundled skill-creator for direct skill authoring in ClawX desktop');
|
||||
}
|
||||
|
||||
if (skillsModified) {
|
||||
config.skills = skillsObj;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// ── plugins.entries.feishu cleanup ──────────────────────────────
|
||||
// Normalize feishu plugin ids dynamically based on installed manifest.
|
||||
// Different environments may report either "openclaw-lark" or
|
||||
|
||||
+7
-7
@@ -99,12 +99,12 @@
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@homebridge/ciao": "^1.3.7",
|
||||
"@larksuite/openclaw-lark": "2026.5.20",
|
||||
"@larksuite/openclaw-lark": "2026.6.10",
|
||||
"@larksuiteoapi/node-sdk": "^1.61.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@openclaw/discord": "2026.6.5",
|
||||
"@openclaw/qqbot": "2026.6.5",
|
||||
"@openclaw/whatsapp": "2026.6.5",
|
||||
"@openclaw/discord": "2026.6.10",
|
||||
"@openclaw/qqbot": "2026.6.10",
|
||||
"@openclaw/whatsapp": "2026.6.10",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
@@ -121,7 +121,7 @@
|
||||
"@sinclair/typebox": "^0.34.48",
|
||||
"@soimy/dingtalk": "^3.6.3",
|
||||
"@tencent-connect/qqbot-connector": "^1.1.0",
|
||||
"@tencent-weixin/openclaw-weixin": "^2.4.3",
|
||||
"@tencent-weixin/openclaw-weixin": "^2.4.6",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/diff": "^8.0.0",
|
||||
@@ -132,7 +132,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"@wecom/wecom-openclaw-plugin": "^2026.5.14",
|
||||
"@wecom/wecom-openclaw-plugin": "^2026.6.23",
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||
"acpx": "0.5.3",
|
||||
"autoprefixer": "^10.4.24",
|
||||
@@ -158,7 +158,7 @@
|
||||
"monaco-editor": "^0.55.1",
|
||||
"mpg123-decoder": "^1.0.3",
|
||||
"ms": "^2.1.3",
|
||||
"openclaw": "2026.6.5",
|
||||
"openclaw": "2026.6.10",
|
||||
"opusscript": "^0.1.1",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"playwright-core": "1.59.1",
|
||||
|
||||
Generated
+75
-64
@@ -52,8 +52,8 @@ importers:
|
||||
specifier: ^1.3.7
|
||||
version: 1.3.7
|
||||
'@larksuite/openclaw-lark':
|
||||
specifier: 2026.5.20
|
||||
version: 2026.5.20(openclaw@2026.6.5(encoding@0.1.13))
|
||||
specifier: 2026.6.10
|
||||
version: 2026.6.10(openclaw@2026.6.10(encoding@0.1.13))
|
||||
'@larksuiteoapi/node-sdk':
|
||||
specifier: ^1.61.1
|
||||
version: 1.62.0
|
||||
@@ -61,14 +61,14 @@ importers:
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@openclaw/discord':
|
||||
specifier: 2026.6.5
|
||||
version: 2026.6.5(openclaw@2026.6.5(encoding@0.1.13))
|
||||
specifier: 2026.6.10
|
||||
version: 2026.6.10(openclaw@2026.6.10(encoding@0.1.13))
|
||||
'@openclaw/qqbot':
|
||||
specifier: 2026.6.5
|
||||
version: 2026.6.5(openclaw@2026.6.5(encoding@0.1.13))
|
||||
specifier: 2026.6.10
|
||||
version: 2026.6.10(openclaw@2026.6.10(encoding@0.1.13))
|
||||
'@openclaw/whatsapp':
|
||||
specifier: 2026.6.5
|
||||
version: 2026.6.5(openclaw@2026.6.5(encoding@0.1.13))
|
||||
specifier: 2026.6.10
|
||||
version: 2026.6.10(openclaw@2026.6.10(encoding@0.1.13))
|
||||
'@playwright/test':
|
||||
specifier: ^1.56.1
|
||||
version: 1.59.0
|
||||
@@ -113,13 +113,13 @@ importers:
|
||||
version: 0.34.48
|
||||
'@soimy/dingtalk':
|
||||
specifier: ^3.6.3
|
||||
version: 3.6.4(openclaw@2026.6.5(encoding@0.1.13))
|
||||
version: 3.6.4(openclaw@2026.6.10(encoding@0.1.13))
|
||||
'@tencent-connect/qqbot-connector':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@tencent-weixin/openclaw-weixin':
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3(openclaw@2026.6.5(encoding@0.1.13))
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6(openclaw@2026.6.10(encoding@0.1.13))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1
|
||||
@@ -151,8 +151,8 @@ importers:
|
||||
specifier: ^5.1.4
|
||||
version: 5.2.0(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.9.0))
|
||||
'@wecom/wecom-openclaw-plugin':
|
||||
specifier: ^2026.5.14
|
||||
version: 2026.5.14(openclaw@2026.6.5(encoding@0.1.13))
|
||||
specifier: ^2026.6.23
|
||||
version: 2026.6.23(openclaw@2026.6.10(encoding@0.1.13))
|
||||
'@whiskeysockets/baileys':
|
||||
specifier: 7.0.0-rc.9
|
||||
version: 7.0.0-rc.9(audio-decode@2.2.3)(jimp@1.6.1)(sharp@0.34.5)
|
||||
@@ -229,8 +229,8 @@ importers:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
openclaw:
|
||||
specifier: 2026.6.5
|
||||
version: 2026.6.5(encoding@0.1.13)
|
||||
specifier: 2026.6.10
|
||||
version: 2026.6.10(encoding@0.1.13)
|
||||
opusscript:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1
|
||||
@@ -1175,8 +1175,8 @@ packages:
|
||||
'@keyv/serialize@1.1.1':
|
||||
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
|
||||
|
||||
'@larksuite/openclaw-lark@2026.5.20':
|
||||
resolution: {integrity: sha512-4iTK0ZJXtylJFg+kh6gttKDi6vqKjbwTNrWTo/8Zw0VjtbpZQ2wkWIYF5BQNie/Ehqf1S9D5w4pjIwL9bCzaKg==}
|
||||
'@larksuite/openclaw-lark@2026.6.10':
|
||||
resolution: {integrity: sha512-OdNePiG88jRIUrRAx0h3bF2o5UxLD4c9zlK5Wfn1xirYLic6koGpx7xoRFAgvCMd15DzLQQtekkwO6+xkDupBw==}
|
||||
engines: {node: '>=22'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -1436,10 +1436,10 @@ packages:
|
||||
resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
'@openclaw/discord@2026.6.5':
|
||||
resolution: {integrity: sha512-Ww/89ODIdZdWZimNzHWoraJbWOrPIJDB+OfVZcQ5fOnsPNyY1p4RAni72wOOiFVkH+3FwLjniCcxA1eUfDkewA==}
|
||||
'@openclaw/discord@2026.6.10':
|
||||
resolution: {integrity: sha512-NKp/j00l+rk5PC0Lv/0fOIiiQJ1c/OpG9471zqXUDKQie6pQ1Fi9KUZUouyoTMmfLh/n4S0CkEMqrON40eBKXA==}
|
||||
peerDependencies:
|
||||
openclaw: '>=2026.6.5'
|
||||
openclaw: '>=2026.6.10'
|
||||
peerDependenciesMeta:
|
||||
openclaw:
|
||||
optional: true
|
||||
@@ -1461,10 +1461,10 @@ packages:
|
||||
peerDependencies:
|
||||
undici: '>=8.3.0 <9'
|
||||
|
||||
'@openclaw/qqbot@2026.6.5':
|
||||
resolution: {integrity: sha512-vY/AbrWD271ReS/oXck2HeuCOB2W5NcgrVU5CJAo+BSp+tzqDZDMsCe/GIc/lwDOjWQg1Ez7+KGwfrlCmn4tjA==}
|
||||
'@openclaw/qqbot@2026.6.10':
|
||||
resolution: {integrity: sha512-6G1yvO+pzvdO2ByfyuefAFVj2mW4urCpEN5BTxevXvmMuE7+AXhu8F0Z4aeIQOnUvBzzu7ZyDQtQ+gOA1trmkw==}
|
||||
peerDependencies:
|
||||
openclaw: '>=2026.6.5'
|
||||
openclaw: '>=2026.6.10'
|
||||
peerDependenciesMeta:
|
||||
openclaw:
|
||||
optional: true
|
||||
@@ -1475,10 +1475,10 @@ packages:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@openclaw/whatsapp@2026.6.5':
|
||||
resolution: {integrity: sha512-YS/JK5By8AeFQDa6AfqdZk7OzPPWF6AoTV0K6zOdwKsQ7BAFTMTRKaHaniBLttVR3sDe5haLqBdJAvg3jrfBoQ==}
|
||||
'@openclaw/whatsapp@2026.6.10':
|
||||
resolution: {integrity: sha512-k/XrRdZY77SHrdaRwJOEB7/JRbjp4yVgGD/ZNyakjTMqo32XRVtwPBUnj7726rW8Kl5yyOMQQLKFiD9MDfhmPQ==}
|
||||
peerDependencies:
|
||||
openclaw: '>=2026.6.5'
|
||||
openclaw: '>=2026.6.10'
|
||||
peerDependenciesMeta:
|
||||
openclaw:
|
||||
optional: true
|
||||
@@ -2260,11 +2260,11 @@ packages:
|
||||
resolution: {integrity: sha512-3nQ2mdyzPRKpBHjd3QiKZDwNzw1F7fBN+rSq8Xms2gg+JWZR4SY2Zdf+doqTyXdyVjG4Y0QM7IA4U42zT9xxzw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@tencent-weixin/openclaw-weixin@2.4.3':
|
||||
resolution: {integrity: sha512-dPQbidUNWigC6V10vGW4i+GLH09x+6zUhafZRjuxkJ9GDu8o62WBsnUTojp4KqUH756hz+t2v9khiCRSi0dBDw==}
|
||||
'@tencent-weixin/openclaw-weixin@2.4.6':
|
||||
resolution: {integrity: sha512-qw9k3PLTiMWGNjjsknHgcTManH1w4j+Ji1ArWIaYLKCq3aFRsVwcqnPi127bvOoVMJGW4dbyJ8NECEMgoO+iRw==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
openclaw: '>=2026.3.22'
|
||||
openclaw: '>=2026.5.12'
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||
@@ -2533,8 +2533,8 @@ packages:
|
||||
'@wecom/aibot-node-sdk@1.0.6':
|
||||
resolution: {integrity: sha512-WZJN3Q+s+94Qjc0VW8d5W1cVkA3emYxiqf+mNRO9UEHoF40puHvizreNMtudjFhm7mmkYiK5ue/QzNiCk+xwLA==}
|
||||
|
||||
'@wecom/wecom-openclaw-plugin@2026.5.14':
|
||||
resolution: {integrity: sha512-z5fhanCn0PT3m8lHDMQATljFXzIsML2r2nq0nEu3KM93E163CmgbDzcXr3R766du4B0y3i65ZdIPh+tWAFeA8g==}
|
||||
'@wecom/wecom-openclaw-plugin@2026.6.23':
|
||||
resolution: {integrity: sha512-IYxLDLiiYmL/v3oN4WJOvRD+5yis+CS+Rr7mcjGs24UBmRukMr+i0UBghWRW9ub2jnitP6Gs0uqhLMf0kZ9/Tw==}
|
||||
peerDependencies:
|
||||
openclaw: '>=2026.3.28'
|
||||
peerDependenciesMeta:
|
||||
@@ -4242,8 +4242,8 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
libsignal@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7}
|
||||
libsignal@git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
resolution: {commit: bcea72df9ec34d9d9140ab30619cf479c7c144c7, repo: git@github.com:whiskeysockets/libsignal-node.git, type: git}
|
||||
version: 6.0.0
|
||||
|
||||
lie@3.3.0:
|
||||
@@ -4802,8 +4802,8 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
openclaw@2026.6.5:
|
||||
resolution: {integrity: sha512-sRgF0TexfRcJX8Eg0lcL6Jj0YdZbSxUbbp8EbG+qo3v6TtVayE6tKPEs3oCKD7YfYe2C/8Qg26HUxTnycd44ZQ==}
|
||||
openclaw@2026.6.10:
|
||||
resolution: {integrity: sha512-LcooND2tBQw8A+kc1Ujltu3lg30bJ0w7XaeRy7eYzobb8BBdcW6DOGbwJL4vpj1vl9+gjRceOtlh5nh9OARcug==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -5740,6 +5740,10 @@ packages:
|
||||
resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tar@7.5.16:
|
||||
resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
teex@1.0.1:
|
||||
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
|
||||
|
||||
@@ -5924,8 +5928,8 @@ packages:
|
||||
resolution: {integrity: sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
undici@8.3.0:
|
||||
resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==}
|
||||
undici@8.5.0:
|
||||
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
unified@11.0.5:
|
||||
@@ -7374,7 +7378,7 @@ snapshots:
|
||||
|
||||
'@keyv/serialize@1.1.1': {}
|
||||
|
||||
'@larksuite/openclaw-lark@2026.5.20(openclaw@2026.6.5(encoding@0.1.13))':
|
||||
'@larksuite/openclaw-lark@2026.6.10(openclaw@2026.6.10(encoding@0.1.13))':
|
||||
dependencies:
|
||||
'@larksuiteoapi/node-sdk': 1.66.1
|
||||
'@sinclair/typebox': 0.34.49
|
||||
@@ -7382,7 +7386,7 @@ snapshots:
|
||||
undici-types: 8.3.0
|
||||
zod: 4.4.3
|
||||
optionalDependencies:
|
||||
openclaw: 2026.6.5(encoding@0.1.13)
|
||||
openclaw: 2026.6.10(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
@@ -7410,7 +7414,7 @@ snapshots:
|
||||
lodash.pickby: 4.6.0
|
||||
protobufjs: 7.5.8
|
||||
qs: 6.15.0
|
||||
ws: 8.20.1
|
||||
ws: 8.21.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
@@ -7629,26 +7633,26 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
|
||||
'@openclaw/discord@2026.6.5(openclaw@2026.6.5(encoding@0.1.13))':
|
||||
'@openclaw/discord@2026.6.10(openclaw@2026.6.10(encoding@0.1.13))':
|
||||
optionalDependencies:
|
||||
openclaw: 2026.6.5(encoding@0.1.13)
|
||||
openclaw: 2026.6.10(encoding@0.1.13)
|
||||
|
||||
'@openclaw/fs-safe@0.3.0':
|
||||
optionalDependencies:
|
||||
jszip: 3.10.1
|
||||
tar: 7.5.13
|
||||
|
||||
'@openclaw/proxyline@0.3.3(undici@8.3.0)':
|
||||
'@openclaw/proxyline@0.3.3(undici@8.5.0)':
|
||||
dependencies:
|
||||
undici: 8.3.0
|
||||
undici: 8.5.0
|
||||
|
||||
'@openclaw/qqbot@2026.6.5(openclaw@2026.6.5(encoding@0.1.13))':
|
||||
'@openclaw/qqbot@2026.6.10(openclaw@2026.6.10(encoding@0.1.13))':
|
||||
optionalDependencies:
|
||||
openclaw: 2026.6.5(encoding@0.1.13)
|
||||
openclaw: 2026.6.10(encoding@0.1.13)
|
||||
|
||||
'@openclaw/whatsapp@2026.6.5(openclaw@2026.6.5(encoding@0.1.13))':
|
||||
'@openclaw/whatsapp@2026.6.10(openclaw@2026.6.10(encoding@0.1.13))':
|
||||
optionalDependencies:
|
||||
openclaw: 2026.6.5(encoding@0.1.13)
|
||||
openclaw: 2026.6.10(encoding@0.1.13)
|
||||
|
||||
'@pinojs/redact@0.4.0': {}
|
||||
|
||||
@@ -8291,7 +8295,7 @@ snapshots:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
|
||||
'@soimy/dingtalk@3.6.4(openclaw@2026.6.5(encoding@0.1.13))':
|
||||
'@soimy/dingtalk@3.6.4(openclaw@2026.6.10(encoding@0.1.13))':
|
||||
dependencies:
|
||||
axios: 1.13.6(debug@4.4.3)
|
||||
dingtalk-stream: 2.1.5
|
||||
@@ -8300,7 +8304,7 @@ snapshots:
|
||||
pdf-parse: 2.4.5
|
||||
zod: 4.4.3
|
||||
optionalDependencies:
|
||||
openclaw: 2026.6.5(encoding@0.1.13)
|
||||
openclaw: 2026.6.10(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
@@ -8319,11 +8323,11 @@ snapshots:
|
||||
dependencies:
|
||||
qrcode-terminal: 0.12.0
|
||||
|
||||
'@tencent-weixin/openclaw-weixin@2.4.3(openclaw@2026.6.5(encoding@0.1.13))':
|
||||
'@tencent-weixin/openclaw-weixin@2.4.6(openclaw@2026.6.10(encoding@0.1.13))':
|
||||
dependencies:
|
||||
openclaw: 2026.6.5(encoding@0.1.13)
|
||||
openclaw: 2026.6.10(encoding@0.1.13)
|
||||
qrcode-terminal: 0.12.0
|
||||
zod: 4.3.6
|
||||
zod: 4.4.3
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
@@ -8685,13 +8689,13 @@ snapshots:
|
||||
dependencies:
|
||||
axios: 1.13.6(debug@4.4.3)
|
||||
eventemitter3: 5.0.4
|
||||
ws: 8.20.1
|
||||
ws: 8.21.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
- utf-8-validate
|
||||
|
||||
'@wecom/wecom-openclaw-plugin@2026.5.14(openclaw@2026.6.5(encoding@0.1.13))':
|
||||
'@wecom/wecom-openclaw-plugin@2026.6.23(openclaw@2026.6.10(encoding@0.1.13))':
|
||||
dependencies:
|
||||
'@wecom/aibot-node-sdk': 1.0.6
|
||||
fast-xml-parser: 5.7.3
|
||||
@@ -8699,7 +8703,7 @@ snapshots:
|
||||
undici: 7.24.6
|
||||
zod: 4.4.3
|
||||
optionalDependencies:
|
||||
openclaw: 2026.6.5(encoding@0.1.13)
|
||||
openclaw: 2026.6.10(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
@@ -8711,7 +8715,7 @@ snapshots:
|
||||
'@cacheable/node-cache': 1.7.6
|
||||
'@hapi/boom': 9.1.4
|
||||
async-mutex: 0.5.0
|
||||
libsignal: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7
|
||||
libsignal: git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7
|
||||
lru-cache: 11.2.7
|
||||
music-metadata: 11.12.3
|
||||
p-queue: 9.1.0
|
||||
@@ -10697,7 +10701,7 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
libsignal@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
libsignal@git+https://git@github.com:whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7:
|
||||
dependencies:
|
||||
curve25519-js: 0.0.4
|
||||
protobufjs: 7.5.8
|
||||
@@ -11472,7 +11476,7 @@ snapshots:
|
||||
ws: 8.21.0
|
||||
zod: 4.4.3
|
||||
|
||||
openclaw@2026.6.5(encoding@0.1.13):
|
||||
openclaw@2026.6.10(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 0.22.1(zod@4.4.3)
|
||||
'@anthropic-ai/sdk': 0.100.1(zod@4.4.3)
|
||||
@@ -11488,13 +11492,12 @@ snapshots:
|
||||
'@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)
|
||||
'@mozilla/readability': 0.6.0
|
||||
'@openclaw/fs-safe': 0.3.0
|
||||
'@openclaw/proxyline': 0.3.3(undici@8.3.0)
|
||||
'@openclaw/proxyline': 0.3.3(undici@8.5.0)
|
||||
chalk: 5.6.2
|
||||
chokidar: 5.0.0
|
||||
clawpdf: 0.3.0
|
||||
commander: 14.0.3
|
||||
croner: 10.0.1
|
||||
cross-spawn: 7.0.6
|
||||
diff: 9.0.0
|
||||
dotenv: 17.4.2
|
||||
express: 5.2.1
|
||||
@@ -11518,12 +11521,12 @@ snapshots:
|
||||
qrcode: 1.5.4
|
||||
quickjs-wasi: 3.0.0
|
||||
rastermill: 0.3.1
|
||||
tar: 7.5.15
|
||||
tar: 7.5.16
|
||||
tree-sitter-bash: 0.25.1
|
||||
tslog: 4.10.2
|
||||
typebox: 1.1.39
|
||||
typescript: 6.0.3
|
||||
undici: 8.3.0
|
||||
undici: 8.5.0
|
||||
web-push: 3.6.7
|
||||
web-tree-sitter: 0.26.9
|
||||
ws: 8.21.0
|
||||
@@ -12609,6 +12612,14 @@ snapshots:
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
tar@7.5.16:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
minipass: 7.1.3
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
teex@1.0.1:
|
||||
dependencies:
|
||||
streamx: 2.25.0
|
||||
@@ -12771,7 +12782,7 @@ snapshots:
|
||||
|
||||
undici@8.1.0: {}
|
||||
|
||||
undici@8.3.0: {}
|
||||
undici@8.5.0: {}
|
||||
|
||||
unified@11.0.5:
|
||||
dependencies:
|
||||
|
||||
@@ -82,6 +82,8 @@ export interface ChatSession {
|
||||
updatedAt?: number;
|
||||
status?: string;
|
||||
hasActiveRun?: boolean;
|
||||
/** Channel provider that last delivered to this session (e.g. webchat, feishu, discord). */
|
||||
channel?: string;
|
||||
}
|
||||
|
||||
export interface ToolStatus {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { useChatStore } from '@/stores/chat';
|
||||
import { useGatewayStore } from '@/stores/gateway';
|
||||
import { useAgentsStore } from '@/stores/agents';
|
||||
import { getSessionActivityMs, getSessionBucket, type SessionBucketKey } from './session-buckets';
|
||||
import { CHANNEL_NAMES } from '@shared/types/channel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -447,6 +448,8 @@ export function Sidebar() {
|
||||
const agentName = agentNameById[agentId] || agentId;
|
||||
const isEditing = editingSessionKey === s.key;
|
||||
const sessionLabel = getSessionLabel(s.key, s.displayName, s.label);
|
||||
const channelType = s.channel && s.channel !== 'webchat' ? s.channel : null;
|
||||
const channelName = channelType ? CHANNEL_NAMES[channelType as keyof typeof CHANNEL_NAMES] ?? channelType : null;
|
||||
return (
|
||||
<div key={s.key} className="group relative flex items-center">
|
||||
{isEditing ? (
|
||||
@@ -500,6 +503,15 @@ export function Sidebar() {
|
||||
<span className="shrink-0 rounded-full bg-black/[0.04] px-2 py-0.5 text-2xs font-medium text-foreground/70 dark:bg-white/[0.08]">
|
||||
{agentName}
|
||||
</span>
|
||||
{channelType && channelName && (
|
||||
<span
|
||||
title={channelName}
|
||||
aria-label={channelName}
|
||||
className="shrink-0 truncate rounded-full bg-blue-500/10 px-2 py-0.5 text-2xs font-medium text-blue-700 dark:bg-blue-400/10 dark:text-blue-400"
|
||||
>
|
||||
{channelName}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate">{sessionLabel}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
+4
-1
@@ -10,6 +10,7 @@ import { useAgentsStore } from './agents';
|
||||
import type { ChatRuntimeEvent } from '../../shared/chat-runtime-events';
|
||||
import { buildBaselineRunKey, captureBaseline, clearBaselines } from './baseline-cache';
|
||||
import { isCronSessionKey, sessionKeysAreEquivalent } from './chat/cron-session-utils';
|
||||
import { isClawXDesktopSessionKey, shouldIncludeSessionInSidebarList } from './chat/session-key-utils';
|
||||
import { fetchCronSessionHistory } from '@/lib/cron-session-history';
|
||||
import { pickStartupSessionFallback } from './chat/session-selection';
|
||||
import {
|
||||
@@ -2641,7 +2642,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
updatedAt: parseSessionUpdatedAtMs(s.updatedAt),
|
||||
status: parseSessionStatus(s.status),
|
||||
hasActiveRun: typeof s.hasActiveRun === 'boolean' ? s.hasActiveRun : undefined,
|
||||
})).filter((s: ChatSession) => s.key);
|
||||
channel: s.lastChannel ? String(s.lastChannel) : undefined,
|
||||
})).filter((s: ChatSession) => shouldIncludeSessionInSidebarList(s));
|
||||
|
||||
const canonicalBySuffix = new Map<string, string>();
|
||||
for (const session of sessions) {
|
||||
@@ -2684,6 +2686,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
}
|
||||
|
||||
const sessionsWithCurrent = !dedupedSessions.find((s) => s.key === nextSessionKey) && nextSessionKey
|
||||
&& isClawXDesktopSessionKey(nextSessionKey)
|
||||
? [
|
||||
...dedupedSessions,
|
||||
{ key: nextSessionKey, displayName: nextSessionKey },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { hostApi } from '@/lib/host-api';
|
||||
import { clearPendingOptimisticUserMessages, getCanonicalPrefixFromSessions, getMessageText, toMs } from './helpers';
|
||||
import { isClawXDesktopSessionKey, shouldIncludeSessionInSidebarList } from './session-key-utils';
|
||||
import { pickStartupSessionFallback } from './session-selection';
|
||||
import { clearPendingOptimisticUserMessages, getCanonicalPrefixFromSessions, getMessageText, toMs } from './helpers';
|
||||
import { DEFAULT_CANONICAL_PREFIX, DEFAULT_SESSION_KEY, type ChatSession, type RawMessage } from './types';
|
||||
import type { ChatGet, ChatSet, SessionHistoryActions } from './store-api';
|
||||
|
||||
@@ -131,7 +132,8 @@ export function createSessionActions(
|
||||
updatedAt: parseSessionUpdatedAtMs(s.updatedAt),
|
||||
status: parseSessionStatus(s.status),
|
||||
hasActiveRun: typeof s.hasActiveRun === 'boolean' ? s.hasActiveRun : undefined,
|
||||
})).filter((s: ChatSession) => s.key);
|
||||
channel: s.lastChannel ? String(s.lastChannel) : undefined,
|
||||
})).filter((s: ChatSession) => shouldIncludeSessionInSidebarList(s));
|
||||
|
||||
const canonicalBySuffix = new Map<string, string>();
|
||||
for (const session of sessions) {
|
||||
@@ -172,6 +174,7 @@ export function createSessionActions(
|
||||
}
|
||||
|
||||
const sessionsWithCurrent = !dedupedSessions.find((s) => s.key === nextSessionKey) && nextSessionKey
|
||||
&& isClawXDesktopSessionKey(nextSessionKey)
|
||||
? [
|
||||
...dedupedSessions,
|
||||
{ key: nextSessionKey, displayName: nextSessionKey },
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CHANNEL_NAMES } from '@shared/types/channel';
|
||||
import { isCronSessionKey } from './cron-session-utils';
|
||||
import type { ChatSession } from './types';
|
||||
|
||||
const CHANNEL_SESSION_SEGMENTS = new Set<string>(Object.keys(CHANNEL_NAMES));
|
||||
|
||||
/**
|
||||
* OpenClaw channel sessions use `agent:<id>:<channel>:...` (e.g. feishu DM keys).
|
||||
*/
|
||||
export function isChannelSessionKey(sessionKey: string): boolean {
|
||||
if (!sessionKey.startsWith('agent:')) return false;
|
||||
const parts = sessionKey.split(':');
|
||||
if (parts.length < 3) return false;
|
||||
return CHANNEL_SESSION_SEGMENTS.has(parts[2] ?? '');
|
||||
}
|
||||
|
||||
export function isClawXDesktopSessionKey(sessionKey: string): boolean {
|
||||
return !isCronSessionKey(sessionKey) && !isChannelSessionKey(sessionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway may register channel sessions before any real user message (e.g. bot
|
||||
* added to a group, webhook ping). Hide those placeholder entries from ClawX
|
||||
* sidebar — they have no preview text, no derived title, and no display name.
|
||||
*/
|
||||
export function isPlaceholderChannelSession(session: ChatSession): boolean {
|
||||
if (!isChannelSessionKey(session.key)) return false;
|
||||
if (session.lastMessagePreview?.trim()) return false;
|
||||
if (session.derivedTitle?.trim()) return false;
|
||||
if (session.displayName?.trim() && session.displayName !== session.key) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shouldIncludeSessionInSidebarList(session: ChatSession): boolean {
|
||||
if (!session.key) return false;
|
||||
if (isChannelSessionKey(session.key)) {
|
||||
return !isPlaceholderChannelSession(session);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isCronSessionKey } from './cron-session-utils';
|
||||
import { isChannelSessionKey } from './session-key-utils';
|
||||
import type { ChatSession } from './types';
|
||||
|
||||
function getAgentIdFromSessionKey(sessionKey: string): string {
|
||||
@@ -28,11 +29,15 @@ export function pickStartupSessionFallback(
|
||||
if (agentMain) return agentMain.key;
|
||||
|
||||
const agentNonCron = sortByUpdatedAtDesc(
|
||||
sessions.filter((session) => session.key.startsWith(`agent:${agentId}:`) && !isCronSessionKey(session.key)),
|
||||
sessions.filter((session) => session.key.startsWith(`agent:${agentId}:`)
|
||||
&& !isCronSessionKey(session.key)
|
||||
&& !isChannelSessionKey(session.key)),
|
||||
);
|
||||
if (agentNonCron.length > 0) return agentNonCron[0]!.key;
|
||||
|
||||
const nonCron = sortByUpdatedAtDesc(sessions.filter((session) => !isCronSessionKey(session.key)));
|
||||
const nonCron = sortByUpdatedAtDesc(
|
||||
sessions.filter((session) => !isCronSessionKey(session.key) && !isChannelSessionKey(session.key)),
|
||||
);
|
||||
if (nonCron.length > 0) return nonCron[0]!.key;
|
||||
|
||||
return null;
|
||||
|
||||
@@ -119,6 +119,88 @@ describe('chat store loadSessions startup selection', () => {
|
||||
expect(useChatStore.getState().messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('hides placeholder feishu sessions but keeps real desktop history', async () => {
|
||||
gatewayRpcMock.mockImplementation(async (method: string) => {
|
||||
if (method === 'sessions.list') {
|
||||
return {
|
||||
sessions: [
|
||||
{
|
||||
key: 'agent:main:feishu:ou_69c24802fa248625f7965a',
|
||||
updatedAt: 9_000,
|
||||
},
|
||||
{
|
||||
key: 'agent:main:session-a',
|
||||
displayName: 'Desktop chat',
|
||||
updatedAt: 5_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (method === 'chat.history') {
|
||||
return { messages: [] };
|
||||
}
|
||||
throw new Error(`Unexpected gateway RPC: ${method}`);
|
||||
});
|
||||
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
currentAgentId: 'main',
|
||||
sessions: [],
|
||||
messages: [],
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
});
|
||||
|
||||
await useChatStore.getState().loadSessions();
|
||||
|
||||
expect(useChatStore.getState().sessions.map((session) => session.key)).toEqual(['agent:main:session-a']);
|
||||
expect(useChatStore.getState().currentSessionKey).toBe('agent:main:session-a');
|
||||
});
|
||||
|
||||
it('shows feishu sessions when they contain real channel messages', async () => {
|
||||
gatewayRpcMock.mockImplementation(async (method: string) => {
|
||||
if (method === 'sessions.list') {
|
||||
return {
|
||||
sessions: [
|
||||
{
|
||||
key: 'agent:main:feishu:ou_69c24802fa248625f7965a',
|
||||
lastMessagePreview: '你好,来自飞书',
|
||||
updatedAt: 9_000,
|
||||
},
|
||||
{
|
||||
key: 'agent:main:session-a',
|
||||
displayName: 'Desktop chat',
|
||||
updatedAt: 5_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (method === 'chat.history') {
|
||||
return { messages: [] };
|
||||
}
|
||||
throw new Error(`Unexpected gateway RPC: ${method}`);
|
||||
});
|
||||
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
useChatStore.setState({
|
||||
currentSessionKey: 'agent:main:main',
|
||||
currentAgentId: 'main',
|
||||
sessions: [],
|
||||
messages: [],
|
||||
sessionLabels: {},
|
||||
sessionLastActivity: {},
|
||||
});
|
||||
|
||||
await useChatStore.getState().loadSessions();
|
||||
|
||||
expect(useChatStore.getState().sessions.map((session) => session.key)).toEqual([
|
||||
'agent:main:feishu:ou_69c24802fa248625f7965a',
|
||||
'agent:main:session-a',
|
||||
]);
|
||||
expect(useChatStore.getState().currentSessionKey).toBe('agent:main:session-a');
|
||||
});
|
||||
|
||||
it('keeps the default main ghost session when only cron sessions exist', async () => {
|
||||
gatewayRpcMock.mockImplementation(async (method: string) => {
|
||||
if (method === 'sessions.list') {
|
||||
|
||||
@@ -30,6 +30,15 @@ describe('pickStartupSessionFallback', () => {
|
||||
expect(pickStartupSessionFallback('agent:main:main', sessions)).toBeNull();
|
||||
});
|
||||
|
||||
it('does not auto-select feishu channel sessions on startup', () => {
|
||||
const sessions: ChatSession[] = [
|
||||
{ key: 'agent:main:feishu:ou_abc', lastMessagePreview: '你好', updatedAt: 9_000 },
|
||||
{ key: 'agent:main:session-new', updatedAt: 5_000 },
|
||||
];
|
||||
|
||||
expect(pickStartupSessionFallback('agent:main:main', sessions)).toBe('agent:main:session-new');
|
||||
});
|
||||
|
||||
it('falls back to non-cron sessions from other agents before cron', () => {
|
||||
const sessions: ChatSession[] = [
|
||||
{ key: 'agent:main:cron:heartbeat', updatedAt: 9_000 },
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Bisection tests for 0d794cd ("fix per channel per session") vs de3046a.
|
||||
*
|
||||
* Part A — dmScope effect is simulated by Gateway event sessionKey alignment
|
||||
* (de3046a production used dmScope=main → events on agent:main:main while UI
|
||||
* showed feishu keys; 0d794cd sets per-channel-peer → keys match).
|
||||
*
|
||||
* Part B — sessions.subscribe adds handleGatewaySessionsChanged → loadSessions.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const hostApiMock = vi.hoisted(() => ({
|
||||
gateway: {
|
||||
status: vi.fn(),
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
restart: vi.fn(),
|
||||
health: vi.fn(),
|
||||
controlUi: vi.fn(),
|
||||
rpc: vi.fn(),
|
||||
},
|
||||
settings: {
|
||||
getAll: vi.fn(),
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
setMany: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
},
|
||||
logs: {
|
||||
recent: vi.fn(),
|
||||
dir: vi.fn(),
|
||||
listFiles: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
},
|
||||
}));
|
||||
const hostEventSubscriptionMock = vi.fn();
|
||||
|
||||
function flushAsyncImports(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
function captureHandlers() {
|
||||
const handlers = new Map<string, (payload: unknown) => void>();
|
||||
hostEventSubscriptionMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => {
|
||||
handlers.set(eventName, handler);
|
||||
return () => {};
|
||||
});
|
||||
return handlers;
|
||||
}
|
||||
|
||||
vi.mock('@/lib/host-api', () => ({
|
||||
hostApi: hostApiMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/host-events', () => ({
|
||||
hostEvents: {
|
||||
onGatewayStatus: (handler: unknown) => hostEventSubscriptionMock('gateway:status', handler),
|
||||
onGatewayError: (handler: unknown) => hostEventSubscriptionMock('gateway:error', handler),
|
||||
onGatewayNotification: (handler: unknown) => hostEventSubscriptionMock('gateway:notification', handler),
|
||||
onGatewayHealth: (handler: unknown) => hostEventSubscriptionMock('gateway:health', handler),
|
||||
onGatewayPresence: (handler: unknown) => hostEventSubscriptionMock('gateway:presence', handler),
|
||||
onGatewayChatMessage: (handler: unknown) => hostEventSubscriptionMock('gateway:chat-message', handler),
|
||||
onGatewaySessionsChanged: (handler: unknown) => hostEventSubscriptionMock('gateway:sessions-changed', handler),
|
||||
onChatRuntimeEvent: (handler: unknown) => hostEventSubscriptionMock('chat:runtime-event', handler),
|
||||
onGatewayChannelStatus: (handler: unknown) => hostEventSubscriptionMock('gateway:channel-status', handler),
|
||||
},
|
||||
}));
|
||||
|
||||
const FEISHU_KEY = 'agent:main:feishu:direct:ou_test';
|
||||
const MAIN_KEY = 'agent:main:main';
|
||||
const OTHER_FEISHU_KEY = 'agent:main:feishu:direct:ou_other';
|
||||
|
||||
describe('bisection 0d794cd vs de3046a', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
hostApiMock.gateway.status.mockResolvedValue({ state: 'running', port: 18789, gatewayReady: true });
|
||||
});
|
||||
|
||||
async function initGatewayHandlers() {
|
||||
const handlers = captureHandlers();
|
||||
const { useGatewayStore } = await import('@/stores/gateway');
|
||||
await useGatewayStore.getState().init();
|
||||
return handlers;
|
||||
}
|
||||
|
||||
function subscribedEvents(): string[] {
|
||||
return hostEventSubscriptionMock.mock.calls.map(([eventName]) => String(eventName));
|
||||
}
|
||||
|
||||
function hasSessionsChangedWiring(): boolean {
|
||||
return subscribedEvents().includes('gateway:sessions-changed');
|
||||
}
|
||||
|
||||
describe('Part A — dmScope key alignment (simulated via runtime event sessionKey)', () => {
|
||||
it('de3046a baseline: run.started on main key does NOT reload history for feishu view', async () => {
|
||||
const handlers = await initGatewayHandlers();
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
const loadHistory = vi.fn(async () => {});
|
||||
useChatStore.setState({
|
||||
currentSessionKey: FEISHU_KEY,
|
||||
sessions: [{ key: FEISHU_KEY }],
|
||||
sending: true,
|
||||
activeRunId: 'run-user',
|
||||
lastUserMessageAt: Date.now(),
|
||||
loadHistory,
|
||||
});
|
||||
|
||||
handlers.get('chat:runtime-event')?.({
|
||||
type: 'run.started',
|
||||
runId: 'run-inbound',
|
||||
sessionKey: MAIN_KEY,
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
await flushAsyncImports();
|
||||
|
||||
expect(loadHistory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('0d794cd with dmScope: aligned run.started DOES reload history (regression trigger)', async () => {
|
||||
const handlers = await initGatewayHandlers();
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
const loadHistory = vi.fn(async () => {});
|
||||
useChatStore.setState({
|
||||
currentSessionKey: FEISHU_KEY,
|
||||
sessions: [{ key: FEISHU_KEY }],
|
||||
sending: true,
|
||||
activeRunId: 'run-user',
|
||||
lastUserMessageAt: Date.now(),
|
||||
loadHistory,
|
||||
});
|
||||
|
||||
handlers.get('chat:runtime-event')?.({
|
||||
type: 'run.started',
|
||||
runId: 'run-user',
|
||||
sessionKey: FEISHU_KEY,
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
await flushAsyncImports();
|
||||
|
||||
expect(loadHistory).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('0d794cd with dmScope: aligned run.ended also reloads history', async () => {
|
||||
const handlers = await initGatewayHandlers();
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
const loadHistory = vi.fn(async () => {});
|
||||
useChatStore.setState({
|
||||
currentSessionKey: FEISHU_KEY,
|
||||
sessions: [{ key: FEISHU_KEY }],
|
||||
sending: true,
|
||||
activeRunId: 'run-user',
|
||||
lastUserMessageAt: Date.now(),
|
||||
loadHistory,
|
||||
});
|
||||
|
||||
handlers.get('chat:runtime-event')?.({
|
||||
type: 'run.ended',
|
||||
runId: 'run-user',
|
||||
sessionKey: FEISHU_KEY,
|
||||
status: 'completed',
|
||||
endedAt: Date.now(),
|
||||
});
|
||||
await flushAsyncImports();
|
||||
|
||||
expect(loadHistory).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Part B — sessions.subscribe (sessions.changed handler)', () => {
|
||||
it('records whether gateway:sessions-changed is wired (de3046a=false, 0d794cd=true)', async () => {
|
||||
await initGatewayHandlers();
|
||||
const wired = hasSessionsChangedWiring();
|
||||
// eslint-disable-next-line no-console -- bisection harness output
|
||||
console.log(`[bisect] gateway:sessions-changed wired=${wired}`);
|
||||
expect([true, false]).toContain(wired);
|
||||
});
|
||||
|
||||
it('other-session sessions.changed triggers loadSessions only when wired', async () => {
|
||||
const handlers = await initGatewayHandlers();
|
||||
const wired = hasSessionsChangedWiring();
|
||||
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
const loadSessions = vi.fn(async () => {});
|
||||
useChatStore.setState({
|
||||
currentSessionKey: FEISHU_KEY,
|
||||
sessions: [{ key: FEISHU_KEY }],
|
||||
loadSessions,
|
||||
});
|
||||
|
||||
handlers.get('gateway:sessions-changed')?.({
|
||||
sessionKey: OTHER_FEISHU_KEY,
|
||||
phase: 'start',
|
||||
ts: Date.now(),
|
||||
});
|
||||
await flushAsyncImports();
|
||||
|
||||
if (wired) {
|
||||
expect(loadSessions).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(handlers.has('gateway:sessions-changed')).toBe(false);
|
||||
expect(loadSessions).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('current-session sessions.changed skips loadSessions when wired', async () => {
|
||||
const handlers = await initGatewayHandlers();
|
||||
if (!hasSessionsChangedWiring()) {
|
||||
expect(handlers.has('gateway:sessions-changed')).toBe(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
const loadSessions = vi.fn(async () => {});
|
||||
useChatStore.setState({
|
||||
currentSessionKey: FEISHU_KEY,
|
||||
sessions: [{ key: FEISHU_KEY }],
|
||||
loadSessions,
|
||||
});
|
||||
|
||||
handlers.get('gateway:sessions-changed')?.({
|
||||
sessionKey: FEISHU_KEY,
|
||||
phase: 'start',
|
||||
ts: Date.now(),
|
||||
});
|
||||
await flushAsyncImports();
|
||||
|
||||
expect(loadSessions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loadSessions reconcile can clear in-flight sending (path exists on both commits; 0d794cd triggers it via sessions.changed)', async () => {
|
||||
await initGatewayHandlers();
|
||||
// updatedAt newer than the in-flight send clears run lifecycle.
|
||||
const lastUserMessageAt = 1_779_693_769_991;
|
||||
hostApiMock.gateway.rpc.mockResolvedValue({
|
||||
sessions: [{
|
||||
key: FEISHU_KEY,
|
||||
updatedAt: 1_779_694_521_057,
|
||||
status: 'done',
|
||||
hasActiveRun: false,
|
||||
lastMessagePreview: 'hello from feishu',
|
||||
}],
|
||||
});
|
||||
|
||||
const { useChatStore } = await import('@/stores/chat');
|
||||
useChatStore.setState({
|
||||
currentSessionKey: FEISHU_KEY,
|
||||
sessions: [{ key: FEISHU_KEY }],
|
||||
sending: true,
|
||||
activeRunId: 'run-active',
|
||||
pendingFinal: true,
|
||||
lastUserMessageAt,
|
||||
});
|
||||
|
||||
await useChatStore.getState().loadSessions();
|
||||
|
||||
expect(useChatStore.getState().sending).toBe(false);
|
||||
expect(useChatStore.getState().activeRunId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -378,6 +378,15 @@ describe('sanitizeOpenClawConfig', () => {
|
||||
const tools = result.tools as Record<string, unknown>;
|
||||
expect(tools.profile).toBe('full');
|
||||
expect(tools.deny).toEqual(['skill_workshop']);
|
||||
const gateway = result.gateway as Record<string, unknown>;
|
||||
const gatewayTools = gateway.tools as Record<string, unknown>;
|
||||
expect(gatewayTools.deny).toEqual(['skill_workshop']);
|
||||
const skills = result.skills as Record<string, unknown>;
|
||||
const workshop = skills.workshop as Record<string, unknown>;
|
||||
const autonomous = workshop.autonomous as Record<string, unknown>;
|
||||
expect(autonomous.enabled).toBe(false);
|
||||
const entries = skills.entries as Record<string, Record<string, unknown>>;
|
||||
expect(entries['skill-creator'].enabled).toBe(true);
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
@@ -407,6 +416,11 @@ describe('sanitizeOpenClawConfig', () => {
|
||||
const tools = result.tools as Record<string, unknown>;
|
||||
expect(tools.profile).toBe('full');
|
||||
expect(tools.deny).toEqual(['skill_workshop']);
|
||||
const gateway = result.gateway as Record<string, unknown>;
|
||||
expect((gateway.tools as Record<string, unknown>).deny).toEqual(['skill_workshop']);
|
||||
const skills = result.skills as Record<string, unknown>;
|
||||
expect(((skills.workshop as Record<string, unknown>).autonomous as Record<string, unknown>).enabled).toBe(false);
|
||||
expect((skills.entries as Record<string, Record<string, unknown>>)['skill-creator'].enabled).toBe(true);
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
@@ -424,6 +438,8 @@ describe('sanitizeOpenClawConfig', () => {
|
||||
const result = await readOpenClawJson();
|
||||
const tools = result.tools as Record<string, unknown>;
|
||||
expect(tools.deny).toEqual(['browser', 'skill_workshop']);
|
||||
const gateway = result.gateway as Record<string, unknown>;
|
||||
expect((gateway.tools as Record<string, unknown>).deny).toEqual(['skill_workshop']);
|
||||
});
|
||||
|
||||
it('migrates legacy tools.web.search.kimi into moonshot plugin config', async () => {
|
||||
|
||||
@@ -26,7 +26,11 @@ async function readConfig(): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function withClawXToolDefaults<T extends Record<string, unknown>>(config: T): T & { tools: Record<string, unknown> } {
|
||||
function withClawXToolDefaults<T extends Record<string, unknown>>(config: T): T & {
|
||||
tools: Record<string, unknown>;
|
||||
gateway: Record<string, unknown>;
|
||||
skills: Record<string, unknown>;
|
||||
} {
|
||||
const tools = (config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools))
|
||||
? { ...(config.tools as Record<string, unknown>) }
|
||||
: {};
|
||||
@@ -48,9 +52,56 @@ function withClawXToolDefaults<T extends Record<string, unknown>>(config: T): T
|
||||
tools.exec = exec;
|
||||
tools.deny = deny.includes('skill_workshop') ? deny : [...deny, 'skill_workshop'];
|
||||
|
||||
const gateway = (config.gateway && typeof config.gateway === 'object' && !Array.isArray(config.gateway))
|
||||
? { ...(config.gateway as Record<string, unknown>) }
|
||||
: {};
|
||||
const gatewayTools = (gateway.tools && typeof gateway.tools === 'object' && !Array.isArray(gateway.tools))
|
||||
? { ...(gateway.tools as Record<string, unknown>) }
|
||||
: {};
|
||||
const gatewayDeny = Array.isArray(gatewayTools.deny)
|
||||
? (gatewayTools.deny as unknown[]).filter((value): value is string => typeof value === 'string')
|
||||
: [];
|
||||
gatewayTools.deny = gatewayDeny.includes('skill_workshop') ? gatewayDeny : [...gatewayDeny, 'skill_workshop'];
|
||||
gateway.tools = gatewayTools;
|
||||
|
||||
const skills = (config.skills && typeof config.skills === 'object' && !Array.isArray(config.skills))
|
||||
? { ...(config.skills as Record<string, unknown>) }
|
||||
: {};
|
||||
const workshop = (skills.workshop && typeof skills.workshop === 'object' && !Array.isArray(skills.workshop))
|
||||
? { ...(skills.workshop as Record<string, unknown>) }
|
||||
: {};
|
||||
const autonomous = (workshop.autonomous && typeof workshop.autonomous === 'object' && !Array.isArray(workshop.autonomous))
|
||||
? { ...(workshop.autonomous as Record<string, unknown>) }
|
||||
: {};
|
||||
autonomous.enabled = false;
|
||||
workshop.autonomous = autonomous;
|
||||
skills.workshop = workshop;
|
||||
|
||||
const entries = (skills.entries && typeof skills.entries === 'object' && !Array.isArray(skills.entries))
|
||||
? { ...(skills.entries as Record<string, unknown>) }
|
||||
: {};
|
||||
const skillCreatorEntry = (entries['skill-creator'] && typeof entries['skill-creator'] === 'object' && !Array.isArray(entries['skill-creator']))
|
||||
? { ...(entries['skill-creator'] as Record<string, unknown>) }
|
||||
: {};
|
||||
skillCreatorEntry.enabled = true;
|
||||
entries['skill-creator'] = skillCreatorEntry;
|
||||
skills.entries = entries;
|
||||
|
||||
return {
|
||||
...config,
|
||||
tools,
|
||||
gateway,
|
||||
skills,
|
||||
session: {
|
||||
...((config.session && typeof config.session === 'object' && !Array.isArray(config.session))
|
||||
? (config.session as Record<string, unknown>)
|
||||
: {}),
|
||||
dmScope: ((config.session && typeof config.session === 'object' && !Array.isArray(config.session))
|
||||
? (config.session as Record<string, unknown>).dmScope
|
||||
: undefined) === 'per-account-channel-peer'
|
||||
? 'per-account-channel-peer'
|
||||
: 'per-channel-peer',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -376,6 +427,82 @@ async function sanitizeConfig(
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Mirror: session.dmScope
|
||||
const sessionConfig = (
|
||||
config.session && typeof config.session === 'object' && !Array.isArray(config.session)
|
||||
? { ...(config.session as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
if (sessionConfig.dmScope !== 'per-channel-peer' && sessionConfig.dmScope !== 'per-account-channel-peer') {
|
||||
sessionConfig.dmScope = 'per-channel-peer';
|
||||
config.session = sessionConfig;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
const gateway = (
|
||||
config.gateway && typeof config.gateway === 'object'
|
||||
? { ...(config.gateway as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const gatewayTools = (
|
||||
gateway.tools && typeof gateway.tools === 'object'
|
||||
? { ...(gateway.tools as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const gatewayDeny = Array.isArray(gatewayTools.deny)
|
||||
? gatewayTools.deny.filter((value): value is string => typeof value === 'string')
|
||||
: [];
|
||||
if (!gatewayDeny.includes('skill_workshop')) {
|
||||
gatewayTools.deny = [...gatewayDeny, 'skill_workshop'];
|
||||
gateway.tools = gatewayTools;
|
||||
config.gateway = gateway;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
let skillsConfig = (
|
||||
config.skills && typeof config.skills === 'object' && !Array.isArray(config.skills)
|
||||
? { ...(config.skills as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
let skillsConfigModified = false;
|
||||
|
||||
const workshop = (
|
||||
skillsConfig.workshop && typeof skillsConfig.workshop === 'object'
|
||||
? { ...(skillsConfig.workshop as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const autonomous = (
|
||||
workshop.autonomous && typeof workshop.autonomous === 'object'
|
||||
? { ...(workshop.autonomous as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
if (autonomous.enabled !== false) {
|
||||
autonomous.enabled = false;
|
||||
workshop.autonomous = autonomous;
|
||||
skillsConfig.workshop = workshop;
|
||||
skillsConfigModified = true;
|
||||
}
|
||||
|
||||
const skillEntries = (
|
||||
skillsConfig.entries && typeof skillsConfig.entries === 'object' && !Array.isArray(skillsConfig.entries)
|
||||
? { ...(skillsConfig.entries as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, Record<string, unknown>>;
|
||||
const skillCreatorEntry = skillEntries['skill-creator'] || {};
|
||||
if (skillCreatorEntry.enabled !== true) {
|
||||
skillEntries['skill-creator'] = {
|
||||
...skillCreatorEntry,
|
||||
enabled: true,
|
||||
};
|
||||
skillsConfig.entries = skillEntries;
|
||||
skillsConfigModified = true;
|
||||
}
|
||||
|
||||
if (skillsConfigModified) {
|
||||
config.skills = skillsConfig;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Mirror: remove stale tools.web.search.kimi.apiKey when moonshot provider exists.
|
||||
const providers = ((config.models as Record<string, unknown> | undefined)?.providers as Record<string, unknown> | undefined) || {};
|
||||
if (providers.moonshot) {
|
||||
@@ -455,8 +582,8 @@ describe('sanitizeOpenClawConfig (blocklist approach)', () => {
|
||||
const entries = skills.entries as Record<string, Record<string, unknown>>;
|
||||
expect(entries['my-skill'].enabled).toBe(true);
|
||||
expect(entries['my-skill'].apiKey).toBe('abc');
|
||||
// Other top-level sections are untouched
|
||||
expect(result.gateway).toEqual({ mode: 'local' });
|
||||
// Other top-level sections are untouched (gateway gets Skill Workshop hardening)
|
||||
expect(result.gateway).toEqual(withClawXToolDefaults({ gateway: { mode: 'local' } }).gateway);
|
||||
});
|
||||
|
||||
it('removes skills.disabled at the root level of skills', async () => {
|
||||
@@ -594,7 +721,12 @@ describe('sanitizeOpenClawConfig (blocklist approach)', () => {
|
||||
// All other sections unchanged
|
||||
expect(result.channels).toEqual({ discord: { token: 'abc', enabled: true } });
|
||||
expect(result.plugins).toEqual({ entries: { customPlugin: { enabled: true } } });
|
||||
expect(result.gateway).toEqual({ mode: 'local', auth: { token: 'xyz' } });
|
||||
expect(result.gateway).toEqual(withClawXToolDefaults({
|
||||
gateway: {
|
||||
mode: 'local',
|
||||
auth: { token: 'xyz' },
|
||||
},
|
||||
}).gateway);
|
||||
expect(result.agents).toEqual({ defaults: { model: { primary: 'gpt-4' } } });
|
||||
});
|
||||
|
||||
@@ -713,7 +845,7 @@ describe('sanitizeOpenClawConfig (blocklist approach)', () => {
|
||||
// Other plugin config is preserved
|
||||
expect(plugins.entries).toEqual({ customPlugin: { enabled: true } });
|
||||
// Other top-level sections untouched
|
||||
expect(result.gateway).toEqual({ mode: 'local' });
|
||||
expect(result.gateway).toEqual(withClawXToolDefaults({ gateway: { mode: 'local' } }).gateway);
|
||||
});
|
||||
|
||||
it('keeps configured built-in channels in plugins.allow when external plugins are enabled', async () => {
|
||||
@@ -1038,4 +1170,42 @@ describe('sanitizeOpenClawConfig (blocklist approach)', () => {
|
||||
const modified = await sanitizeConfig(configPath, { all: ['browser'], enabledByDefault: ['browser'] });
|
||||
expect(modified).toBe(false);
|
||||
});
|
||||
|
||||
it('sets session.dmScope to per-channel-peer when unset', async () => {
|
||||
await writeConfig(withClawXToolDefaults({}));
|
||||
|
||||
const modified = await sanitizeConfig(configPath, { all: ['browser'], enabledByDefault: ['browser'] });
|
||||
expect(modified).toBe(false);
|
||||
|
||||
const result = await readConfig();
|
||||
expect((result.session as Record<string, unknown>).dmScope).toBe('per-channel-peer');
|
||||
});
|
||||
|
||||
it('preserves session.dmScope when already set to per-account-channel-peer', async () => {
|
||||
await writeConfig(withClawXToolDefaults({
|
||||
session: { dmScope: 'per-account-channel-peer' },
|
||||
}));
|
||||
|
||||
const modified = await sanitizeConfig(configPath, { all: ['browser'], enabledByDefault: ['browser'] });
|
||||
expect(modified).toBe(false);
|
||||
|
||||
const result = await readConfig();
|
||||
expect((result.session as Record<string, unknown>).dmScope).toBe('per-account-channel-peer');
|
||||
});
|
||||
|
||||
it('overrides session.dmScope when set to main', async () => {
|
||||
// Write config with dmScope: 'main' but otherwise already sanitized,
|
||||
// so only the session.dmScope change should trigger modified=true.
|
||||
const base = withClawXToolDefaults({});
|
||||
await writeConfig({
|
||||
...base,
|
||||
session: { dmScope: 'main' },
|
||||
});
|
||||
|
||||
const modified = await sanitizeConfig(configPath, { all: ['browser'], enabledByDefault: ['browser'] });
|
||||
expect(modified).toBe(true);
|
||||
|
||||
const result = await readConfig();
|
||||
expect((result.session as Record<string, unknown>).dmScope).toBe('per-channel-peer');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
isChannelSessionKey,
|
||||
isClawXDesktopSessionKey,
|
||||
isPlaceholderChannelSession,
|
||||
shouldIncludeSessionInSidebarList,
|
||||
} from '@/stores/chat/session-key-utils';
|
||||
import type { ChatSession } from '@/stores/chat/types';
|
||||
|
||||
describe('session-key-utils', () => {
|
||||
it('detects feishu and other channel session keys', () => {
|
||||
expect(isChannelSessionKey('agent:main:feishu:ou_abc123')).toBe(true);
|
||||
expect(isChannelSessionKey('agent:main:telegram:12345')).toBe(true);
|
||||
expect(isChannelSessionKey('agent:main:whatsapp:dm:abc')).toBe(true);
|
||||
});
|
||||
|
||||
it('treats ClawX desktop session keys as non-channel', () => {
|
||||
expect(isChannelSessionKey('agent:main:main')).toBe(false);
|
||||
expect(isChannelSessionKey('agent:main:session-1710000000000')).toBe(false);
|
||||
expect(isChannelSessionKey('agent:main:cron:heartbeat')).toBe(false);
|
||||
});
|
||||
|
||||
it('excludes cron and channel keys from desktop-only session keys', () => {
|
||||
expect(isClawXDesktopSessionKey('agent:main:main')).toBe(true);
|
||||
expect(isClawXDesktopSessionKey('agent:main:session-1710000000000')).toBe(true);
|
||||
expect(isClawXDesktopSessionKey('agent:main:feishu:ou_abc123')).toBe(false);
|
||||
expect(isClawXDesktopSessionKey('agent:main:cron:heartbeat')).toBe(false);
|
||||
});
|
||||
|
||||
it('detects placeholder channel sessions without any preview/title', () => {
|
||||
const placeholder: ChatSession = {
|
||||
key: 'agent:main:feishu:ou_abc123',
|
||||
};
|
||||
expect(isPlaceholderChannelSession(placeholder)).toBe(true);
|
||||
expect(shouldIncludeSessionInSidebarList(placeholder)).toBe(false);
|
||||
});
|
||||
|
||||
it('includes channel sessions once they have a message preview', () => {
|
||||
const active: ChatSession = {
|
||||
key: 'agent:main:feishu:ou_abc123',
|
||||
lastMessagePreview: 'feishu:ou_abc123',
|
||||
};
|
||||
expect(isPlaceholderChannelSession(active)).toBe(false);
|
||||
expect(shouldIncludeSessionInSidebarList(active)).toBe(true);
|
||||
});
|
||||
|
||||
it('includes channel sessions with a derived title', () => {
|
||||
const titled: ChatSession = {
|
||||
key: 'agent:main:feishu:ou_abc123',
|
||||
derivedTitle: '飞书对话',
|
||||
};
|
||||
expect(isPlaceholderChannelSession(titled)).toBe(false);
|
||||
expect(shouldIncludeSessionInSidebarList(titled)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user