mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 06:30:45 +08:00
fix: [Feature] 支持使用 /workspacelist 序号切换工作区 (#87)
This commit is contained in:
parent
840e5aa857
commit
91f49a603b
12 changed files with 49 additions and 23 deletions
|
|
@ -202,7 +202,7 @@ Do not modify the same profile through a terminal or plugin market during instal
|
|||
| `/cancel` | Cancel batch input and discard its collected messages. |
|
||||
| `/repair` | In a Feishu direct chat, incrementally repair the card callback and permissions required for media and the native Slash Command panel. |
|
||||
| `/compact` | Immediately compact older context in the Session bound to the current chat. |
|
||||
| `/workspace <absolute workspace path>` | Switch the current bot's Harness workspace. |
|
||||
| `/workspace <workspace index or absolute path>` | Switch the current bot's Harness workspace by `/workspacelist` index or absolute path. |
|
||||
| `/workspacelist` | List workspace absolute paths that still exist on the current Harness Host. |
|
||||
| `/sessionlist [workspace number or absolute path]`, `/sessions [...]` | Equivalent aliases that list every registered session ID and title in the selected workspace; omit the argument to use the current workspace. |
|
||||
| `/session <Session ID>` | Bind the current chat to an existing Harness session. |
|
||||
|
|
@ -236,7 +236,7 @@ If the Slack desktop app has no native Slash Command registered with the same na
|
|||
- Feishu `/repair` is available only in a direct chat and follows the current bot's channel access policy just like every other command; the plugin defines no separate administrator role. It incrementally adds the currently missing `card.action.trigger`, `im:message:readonly`, `im:resource`, `application:app_slash_command:read`, and `application:app_slash_command:write`, while the confirmation page shows only items the app is currently missing. The authorization page must be opened by an account that can access the target app in Feishu Open Platform. Bare `/repair` starts repair; if an older attempt is still awaiting authorization, it invalidates that one-time link before generating a new one. Use `/repair qr` for the current link's QR code, `/repair status` to inspect the attempt, `/repair verify` to refresh verification, and `/repair cancel` to cancel it; none of these four supplemental commands starts another authorization. Once Feishu has accepted the update and the bot is waiting for the test-button callback, a second repair is not started concurrently.
|
||||
- `/compact` acts only on the Harness Session already bound to the current chat and is never sent to the model. The bot reports the applicable status when the chat has no Session yet, the Session is generating a reply, or there is no compactable history.
|
||||
- The path must be an existing absolute directory. The bot returns an actionable error and the correct usage when validation fails.
|
||||
- `/workspacelist` takes no arguments. It combines the Harness global registry with the current bot's path. When that current path still exists and is safe to display, it appears first and is marked as current. Any listed path can be copied directly into `/workspace`.
|
||||
- `/workspacelist` takes no arguments. It combines the Harness global registry with the current bot's path. When that current path still exists and is safe to display, it appears first and is marked as current. `/workspace N` switches using the freshly resolved list order at execution time; absolute paths remain supported.
|
||||
- `/sessionlist` and `/sessions` are equivalent. A numeric argument uses the same freshly resolved order as `/workspacelist` at command execution time. An absolute path can also select a workspace directly, and the result echoes the resolved path.
|
||||
- Both session-list aliases include every session registered to the selected workspace. Archived sessions are marked as archived; blank and subagent sessions are included when they belong to that workspace; sessions without a title are shown as `No title yet`. Any listed ID can be passed directly to `/session Session ID`.
|
||||
- `/session` accepts exactly one Session ID obtained from `/sessionlist`. It neither creates a session nor immediately prompts the model; later messages in the current chat continue the bound session. Regular archived sessions can be bound without being unarchived, while subagent sessions cannot be bound.
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ dsh plugin --profile web add -w --save-exact @xmanrui/dsh-im@3.1.0 --registry=ht
|
|||
| `/cancel` | 取消批量输入并丢弃已收集的消息。 |
|
||||
| `/repair` | 在飞书私聊中增量修复卡片回调,并补全媒体与原生 Slash Command 面板所需的权限。 |
|
||||
| `/compact` | 立即压缩当前聊天绑定会话的较早上下文。 |
|
||||
| `/workspace <工作区绝对路径>` | 切换当前机器人的 Harness 工作区。 |
|
||||
| `/workspace <工作区序号或绝对路径>` | 按 `/workspacelist` 序号或绝对路径切换当前机器人的 Harness 工作区。 |
|
||||
| `/workspacelist` | 列出当前 Harness Host 上仍然存在的工作区绝对路径。 |
|
||||
| `/sessionlist [工作区序号或绝对路径]`、`/sessions [...]` | 两个等价命令;列出指定工作区登记的所有会话 ID 和标题,省略参数时使用当前工作区。 |
|
||||
| `/session <Session ID>` | 将当前聊天绑定到指定的已有 Harness 会话。 |
|
||||
|
|
@ -239,7 +239,7 @@ Slack 桌面端若未注册同名的原生 Slash Command,会拦截直接以 `/
|
|||
- 飞书 `/repair` 仅在私聊中可用,并与其他命令一样只服从当前飞书机器人的渠道访问策略;插件不另行区分管理员和普通用户。它增量补全当前缺少的 `card.action.trigger`、`im:message:readonly`、`im:resource`、`application:app_slash_command:read` 和 `application:app_slash_command:write`,确认页只显示当前应用缺少的项。授权页必须由在飞书开放平台中有权访问目标应用的账号打开。普通 `/repair` 会启动修复;若旧任务仍在等待授权,会先作废旧的一次性链接再生成新链接。发送 `/repair qr` 获取当前链接的二维码,`/repair status` 查询当前任务,`/repair verify` 重新查询验证状态,`/repair cancel` 取消任务;这四个补充命令均不会另起授权。平台已接受更新、正在等待测试按钮回调时,不会并发启动第二次修复。
|
||||
- `/compact` 只作用于当前聊天已经绑定的 Harness 会话,不会把命令发送给模型。当前聊天尚未创建会话、会话正在生成回复或没有可压缩历史时,机器人会直接返回对应状态。
|
||||
- 只接受已经存在的绝对目录;路径无效时机器人会返回具体提示和正确用法。
|
||||
- `/workspacelist` 不需要参数。它合并 Harness 全局登记项与当前机器人的路径;当前路径仍存在且可安全显示时会排在首位并标记为“当前”。结果可直接复制到 `/workspace` 命令。
|
||||
- `/workspacelist` 不需要参数。它合并 Harness 全局登记项与当前机器人的路径;当前路径仍存在且可安全显示时会排在首位并标记为“当前”。`/workspace N` 会在执行时按最新列表顺序切换,也可继续使用绝对路径。
|
||||
- `/sessionlist` 和 `/sessions` 完全等价。数字参数按命令执行时与 `/workspacelist` 相同的最新顺序解析;也可使用绝对路径直接指定工作区。结果会回显最终选中的路径。
|
||||
- 两个会话列表命令都会列出该工作区登记的所有会话。已归档会话会标记为“已归档”;空白会话和子代理会话在它们归属该工作区时也会列出;没有标题的会话显示为“暂无标题”。结果中的 ID 可直接用于 `/session Session ID`。
|
||||
- `/session` 只接受一个由 `/sessionlist` 获得的 Session ID。它不会新建会话或立即向模型发送消息;绑定成功后,当前聊天的后续消息会继续该会话。普通归档会话可以绑定但不会自动取消归档,子代理会话不能绑定。
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ const HELP_TEXT_LINES = [
|
|||
'/new 开启一个全新会话',
|
||||
'/compact 压缩当前会话的较早上下文',
|
||||
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspace 工作区序号或绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ function isFeishuLocalCommand(text, { hasImages = false, hasFiles = false } = {}
|
|||
|
||||
/** Canonical workspace/session help advertised by every bridge family. */
|
||||
const WORKSPACE_HELP_LINES = [
|
||||
'/workspace 工作区序号或绝对路径 切换工作区',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function helpText() {
|
|||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
|
|
|
|||
|
|
@ -120,7 +120,8 @@ export default {
|
|||
'{label}机器人已连接 DeepSeek Harness。': 'The {label} bot is connected to DeepSeek Harness.',
|
||||
'/new 开启一个全新会话': '/new Start a brand-new session',
|
||||
'/compact 压缩当前会话的较早上下文': '/compact Compact the earlier context of the current session',
|
||||
'/workspace 工作区绝对路径 切换工作区': '/workspace <absolute workspace path> Switch workspace',
|
||||
'/workspace 工作区序号或绝对路径 切换工作区':
|
||||
'/workspace <workspace index or absolute path> Switch workspace',
|
||||
'/workspacelist 列出工作区绝对路径': '/workspacelist List absolute workspace paths',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题':
|
||||
'/sessionlist [workspace index or absolute path] List session IDs and titles',
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ export default {
|
|||
'当前 Harness Host 上存在的工作区({count}):':
|
||||
'Workspaces on the current Harness Host ({count}):',
|
||||
'(当前)': '(current)',
|
||||
'切换用法:/workspace 工作区绝对路径': 'To switch: /workspace Workspace absolute path',
|
||||
'切换用法:/workspace 工作区序号或绝对路径':
|
||||
'To switch: /workspace Workspace index or absolute path',
|
||||
'查看会话:/sessionlist 工作区序号或绝对路径':
|
||||
'To view Sessions: /sessionlist Workspace index or absolute path',
|
||||
'机器人正在移除或已重新接入,无法列出原会话的工作区。':
|
||||
|
|
@ -80,10 +81,12 @@ export default {
|
|||
'归档:{archived}': 'Archived: {archived}',
|
||||
'是': 'Yes',
|
||||
'否': 'No',
|
||||
'用法:/workspace 工作区绝对路径': 'Usage: /workspace Workspace absolute path',
|
||||
'用法:/workspace 工作区序号或绝对路径':
|
||||
'Usage: /workspace Workspace index or absolute path',
|
||||
'当前机器人暂不支持切换工作区。': 'This bot does not support switching Workspaces yet.',
|
||||
'工作区已切换为:{workspace}': 'Workspace switched to: {workspace}',
|
||||
'{message}\n用法:/workspace 工作区绝对路径': '{message}\nUsage: /workspace Workspace absolute path',
|
||||
'{message}\n用法:/workspace 工作区序号或绝对路径':
|
||||
'{message}\nUsage: /workspace Workspace index or absolute path',
|
||||
'机器人正在移除或已重新接入,无法切换原会话的工作区。':
|
||||
'The bot is being removed or has been reconnected; cannot switch the Workspace of the original Session.',
|
||||
|
||||
|
|
|
|||
|
|
@ -588,7 +588,7 @@ export class TextHarnessBridge {
|
|||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ async function runWorkspaceListCommand(match, harness) {
|
|||
`${index + 1}. ${workspace}${workspace === current ? t('(当前)') : ''}`
|
||||
)),
|
||||
'',
|
||||
t('切换用法:/workspace 工作区绝对路径'),
|
||||
t('切换用法:/workspace 工作区序号或绝对路径'),
|
||||
t('查看会话:/sessionlist 工作区序号或绝对路径'),
|
||||
];
|
||||
const message = lines.join('\n');
|
||||
|
|
@ -366,18 +366,30 @@ export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|||
if (!match) return null;
|
||||
const workspace = match[1]?.trim();
|
||||
if (!workspace) {
|
||||
return commandResult(t('用法:/workspace 工作区绝对路径'));
|
||||
return commandResult(t('用法:/workspace 工作区序号或绝对路径'));
|
||||
}
|
||||
if (typeof harness?.switchWorkspace !== 'function') {
|
||||
return commandResult(t('当前机器人暂不支持切换工作区。'));
|
||||
}
|
||||
try {
|
||||
const current = await harness.switchWorkspace(workspace);
|
||||
let selected = workspace;
|
||||
if (/^\d+$/u.test(workspace)) {
|
||||
if (typeof harness?.listWorkspaces !== 'function') {
|
||||
return commandResult(t('当前机器人暂不支持按序号选择工作区。'));
|
||||
}
|
||||
const { paths } = await workspacePathSnapshot(harness);
|
||||
const position = Number(workspace);
|
||||
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
|
||||
return commandResult(t('工作区序号不存在,请先执行 /workspacelist。'));
|
||||
}
|
||||
selected = paths[position - 1];
|
||||
}
|
||||
const current = await harness.switchWorkspace(selected);
|
||||
return commandResult(t('工作区已切换为:{workspace}', { workspace: current }));
|
||||
} catch (error) {
|
||||
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
||||
return commandResult(t(`{message}
|
||||
用法:/workspace 工作区绝对路径`, { message: error.message }));
|
||||
用法:/workspace 工作区序号或绝对路径`, { message: error.message }));
|
||||
}
|
||||
if (error?.code === 'workspace-bot-not-found') {
|
||||
return commandResult(t('机器人正在移除或已重新接入,无法切换原会话的工作区。'));
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function helpText() {
|
|||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const HELP_TEXT = () => [
|
|||
t('/new 开启一个全新会话'),
|
||||
t('/compact 压缩当前会话的较早上下文'),
|
||||
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
||||
t('/workspace 工作区绝对路径 切换工作区'),
|
||||
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
||||
t('/workspacelist 列出工作区绝对路径'),
|
||||
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
||||
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
||||
|
|
|
|||
|
|
@ -940,10 +940,15 @@ test('config-store removal observation retires workspaces after the config commi
|
|||
assert.equal(workspaces.has('bot_feishu'), false);
|
||||
});
|
||||
|
||||
test('/workspace command preserves spaces and returns actionable validation messages', async (t) => {
|
||||
const { alternateWorkspace } = await fixture(t);
|
||||
test('/workspace command supports fresh list numbers, preserves paths, and returns actionable errors', async (t) => {
|
||||
const { defaultWorkspace, alternateWorkspace } = await fixture(t);
|
||||
const switched = [];
|
||||
const harness = { async switchWorkspace(path) { switched.push(path); return path; } };
|
||||
let listed = [alternateWorkspace];
|
||||
const harness = {
|
||||
currentWorkspace() { return defaultWorkspace; },
|
||||
async listWorkspaces() { return listed; },
|
||||
async switchWorkspace(path) { switched.push(path); return path; },
|
||||
};
|
||||
|
||||
assert.equal(await runWorkspaceCommand('hello', harness), null);
|
||||
assert.match((await runWorkspaceCommand('/workspace', harness)).message, /用法/);
|
||||
|
|
@ -951,7 +956,11 @@ test('/workspace command preserves spaces and returns actionable validation mess
|
|||
(await runWorkspaceCommand(`/workspace ${alternateWorkspace}`, harness)).message,
|
||||
new RegExp(alternateWorkspace.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
);
|
||||
assert.deepEqual(switched, [alternateWorkspace]);
|
||||
assert.match((await runWorkspaceCommand('/workspace 2', harness)).message, new RegExp(alternateWorkspace));
|
||||
listed = [];
|
||||
assert.match((await runWorkspaceCommand('/workspace 1', harness)).message, new RegExp(defaultWorkspace));
|
||||
assert.match((await runWorkspaceCommand('/workspace 2', harness)).message, /workspacelist/);
|
||||
assert.deepEqual(switched, [alternateWorkspace, alternateWorkspace, defaultWorkspace]);
|
||||
|
||||
const invalidHarness = {
|
||||
async switchWorkspace() {
|
||||
|
|
@ -962,7 +971,7 @@ test('/workspace command preserves spaces and returns actionable validation mess
|
|||
};
|
||||
const invalid = await runWorkspaceCommand('/workspace /missing/workspace', invalidHarness);
|
||||
assert.match(invalid.message, /路径不存在/);
|
||||
assert.match(invalid.message, /用法:\/workspace 工作区绝对路径/);
|
||||
assert.match(invalid.message, /用法:\/workspace 工作区序号或绝对路径/);
|
||||
|
||||
const removedHarness = {
|
||||
async switchWorkspace() {
|
||||
|
|
@ -1004,7 +1013,7 @@ test('/workspacelist returns existing absolute paths with the current workspace
|
|||
assert.ok(result.message.indexOf(defaultWorkspace) < result.message.indexOf(alternateWorkspace));
|
||||
assert.ok(result.message.indexOf(alternateWorkspace) < result.message.indexOf(thirdWorkspace));
|
||||
assert.doesNotMatch(result.message, /missing|relative\/path/);
|
||||
assert.match(result.message, /切换用法:\/workspace 工作区绝对路径/);
|
||||
assert.match(result.message, /切换用法:\/workspace 工作区序号或绝对路径/);
|
||||
assert.match(result.message, /查看会话:\/sessionlist 工作区序号或绝对路径/);
|
||||
assert.equal(result.messages.join(''), result.message);
|
||||
assert.equal(listCalls, 1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue