Upgrade openclaw to 6.10 (#1138)

This commit is contained in:
paisley
2026-06-30 18:29:35 +08:00
committed by GitHub
parent cb8c3cfd02
commit 22af7468d7
15 changed files with 873 additions and 91 deletions
+123 -10
View File
@@ -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
View File
@@ -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",
+75 -64
View File
@@ -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:
+2
View File
@@ -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 {
+12
View File
@@ -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
View File
@@ -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 },
+5 -2
View File
@@ -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 },
+40
View File
@@ -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;
}
+7 -2
View File
@@ -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();
});
});
});
+16
View File
@@ -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 () => {
+175 -5
View File
@@ -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');
});
});
+55
View File
@@ -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);
});
});