@xmanrui/dsh-im 4.33.0 → 4.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/lib/client.js +3 -1
- package/lib/index.js +293 -290
- package/package.json +1 -1
- package/plugin-src/host/modern-harness-api.mjs +22 -0
- package/src/channels/feishu/bridge.mjs +12 -0
- package/src/channels/feishu/feishu-cards.mjs +5 -0
- package/src/channels/feishu/slash-command-panel.mjs +2 -0
- package/src/channels/qq/qq-bridge.mjs +6 -1
- package/src/channels/shared/bot-workspace-store.mjs +5 -0
- package/src/channels/shared/command-list-snapshots.mjs +33 -0
- package/src/channels/shared/harness-client.mjs +6 -0
- package/src/channels/shared/i18n-en/permission.mjs +30 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/permission-command.mjs +149 -0
- package/src/channels/shared/preset-command.mjs +6 -51
- package/src/channels/telegram/telegram-runtime.mjs +12 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.34.0",
|
|
4
4
|
"icon": "./assets/logo-plugin-message-link-gradient.webp",
|
|
5
5
|
"description": "把微信、飞书、企微、钉钉、QQ、Telegram、WhatsApp、Slack、Discord、Matrix接入DSH。 Connect Telegram, WhatsApp, Slack, Discord, Matrix, WeChat, Feishu, WeCom, DingTalk, and QQ to DSH.",
|
|
6
6
|
"keywords": [
|
|
@@ -240,6 +240,9 @@ class ModernHarnessApi {
|
|
|
240
240
|
models: (request, signal) => rpcResult(request, () => this.#sessionModels(
|
|
241
241
|
request.payload.sessionId, signal,
|
|
242
242
|
)),
|
|
243
|
+
permissions: (request, signal) => rpcResult(request, () => this.#sessionPermissions(
|
|
244
|
+
request.payload.sessionId, signal,
|
|
245
|
+
)),
|
|
243
246
|
selectModel: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
244
247
|
'session', 'selectModel', { request: request.payload }, signal,
|
|
245
248
|
)),
|
|
@@ -414,6 +417,25 @@ class ModernHarnessApi {
|
|
|
414
417
|
return this.#invoke('session', 'modelCatalog', {}, signal);
|
|
415
418
|
}
|
|
416
419
|
|
|
420
|
+
async #sessionPermissions(sessionId, signal) {
|
|
421
|
+
const [catalog, projection] = await Promise.all([
|
|
422
|
+
this.#invoke('permissionPresets', 'catalog', {}, signal),
|
|
423
|
+
this.#invoke('session', 'projections', { request: { sessionId } }, signal),
|
|
424
|
+
]);
|
|
425
|
+
if (projection === null) {
|
|
426
|
+
const error = new Error('Session not found');
|
|
427
|
+
error.failure = { code: 'session-not-found', message: error.message, details: {} };
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
const currentValue = projection?.values?.permissions?.currentValue;
|
|
431
|
+
if (typeof currentValue !== 'string' || !Array.isArray(catalog?.options)) {
|
|
432
|
+
const error = new Error('Host permission presets are unavailable');
|
|
433
|
+
error.failure = { code: 'permissions-unavailable', message: error.message, details: {} };
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
return { currentValue, options: catalog.options };
|
|
437
|
+
}
|
|
438
|
+
|
|
417
439
|
async #sessionModels(sessionId, signal) {
|
|
418
440
|
const [catalog, list] = await Promise.all([
|
|
419
441
|
this.#modelCatalog(signal),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPermissionCommand, runPermissionCommand } from '../shared/permission-command.mjs';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import QRCode from 'qrcode';
|
|
3
4
|
import {
|
|
@@ -298,6 +299,7 @@ function printableText(value) {
|
|
|
298
299
|
}
|
|
299
300
|
|
|
300
301
|
function isFeishuLocalCommand(text, { hasImages = false, hasFiles = false } = {}) {
|
|
302
|
+
if (isPermissionCommand(text)) return true;
|
|
301
303
|
if (hasImages || hasFiles || typeof text !== 'string') return false;
|
|
302
304
|
const command = text.trim();
|
|
303
305
|
return MENU_COMMAND.test(command)
|
|
@@ -1234,6 +1236,7 @@ export class FeishuHarnessBridge {
|
|
|
1234
1236
|
return processing;
|
|
1235
1237
|
}
|
|
1236
1238
|
const commandRunner = isHistoryCommand(commandText) ? runHistoryCommand
|
|
1239
|
+
: isPermissionCommand(commandText) ? runPermissionCommand
|
|
1237
1240
|
: hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
|
1238
1241
|
? runControlCommand
|
|
1239
1242
|
: (isModelCommand(commandText)
|
|
@@ -1690,6 +1693,15 @@ export class FeishuHarnessBridge {
|
|
|
1690
1693
|
await this.#handleRepairCommand(event, commandText);
|
|
1691
1694
|
return;
|
|
1692
1695
|
}
|
|
1696
|
+
if (isPermissionCommand(text)) {
|
|
1697
|
+
const result = await runPermissionCommand(text, this.#harness, this.#state, key, {
|
|
1698
|
+
signal: this.#signal, hasImages, hasFiles,
|
|
1699
|
+
});
|
|
1700
|
+
for (const reply of result.messages) {
|
|
1701
|
+
await this.#send(event.message.chat_id, reply, { replyTo: event.message.message_id });
|
|
1702
|
+
}
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1693
1705
|
if (commandText === '/help') {
|
|
1694
1706
|
await this.#send(event.message.chat_id, menuHelpText(), { replyTo: event.message.message_id });
|
|
1695
1707
|
return;
|
|
@@ -535,6 +535,9 @@ export function menuHelpText() {
|
|
|
535
535
|
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
|
|
536
536
|
'纯数字 ID:/preset id:<ID>',
|
|
537
537
|
'/preset --default 跟随 Host 默认',
|
|
538
|
+
'/permissionlist 按序号列出权限档位',
|
|
539
|
+
'/permission [序号或完整ID] 查看或切换当前会话权限',
|
|
540
|
+
'纯数字 ID:/permission id:<ID>',
|
|
538
541
|
'/models 列出模型',
|
|
539
542
|
'/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级',
|
|
540
543
|
'/reasoning [序号、等级ID或 --default] 查看或切换当前推理等级',
|
|
@@ -619,6 +622,8 @@ export function helpCard(extraTextLines = []) {
|
|
|
619
622
|
{ tag: 'hr' },
|
|
620
623
|
{ tag: 'div', text: markdown([
|
|
621
624
|
t(HELP_TEXT_COMMANDS),
|
|
625
|
+
t('`/permissionlist` — 按序号列出权限档位'),
|
|
626
|
+
t('`/permission [序号/ID]` — 查看或切换当前会话权限'),
|
|
622
627
|
t('/ws、/wsl、/workspaces 工作区命令别名'),
|
|
623
628
|
t('`/version` — 查看插件版本'),
|
|
624
629
|
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
|
@@ -49,6 +49,8 @@ export const SLASH_COMMAND_MANIFEST = Object.freeze([
|
|
|
49
49
|
{ command: 'reasoning', icon: 'ai-deepthink_outlined', default: '查看或切换推理等级', en_us: 'Show or switch the reasoning effort' },
|
|
50
50
|
{ command: 'presetlist', icon: 'skill_outlined', default: '列出可用 Agent 预设', en_us: 'List available Agent Presets' },
|
|
51
51
|
{ command: 'preset', icon: 'skill_outlined', default: '查看或切换 Agent 预设', en_us: 'Show or switch the Agent Preset' },
|
|
52
|
+
{ command: 'permissionlist', icon: 'skill_outlined', default: '按序号列出权限档位', en_us: 'List numbered permission presets' },
|
|
53
|
+
{ command: 'permission', icon: 'skill_outlined', default: '查看或切换当前会话权限', en_us: 'Show or switch current session permissions' },
|
|
52
54
|
{ command: 'stop', icon: 'clear_outlined', default: '停止当前任务', en_us: 'Stop the current task' },
|
|
53
55
|
{ command: 'steer', icon: 'promptword_outlined', default: '给当前任务补充指令', en_us: 'Send additional instructions to the current task' },
|
|
54
56
|
{ command: 'batch', icon: 'chat-ai_outlined', default: '开始批量输入(仅私聊)', en_us: 'Start batch input (private chats only)' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPermissionCommand, runPermissionCommand } from '../shared/permission-command.mjs';
|
|
1
2
|
import { createDeferredDeliveryCoordinator, deferredOutcomeText } from '../shared/deferred-delivery-coordinator.mjs';
|
|
2
3
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
3
4
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
@@ -115,6 +116,9 @@ function helpText() {
|
|
|
115
116
|
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
116
117
|
t('纯数字 ID:/preset id:<ID>'),
|
|
117
118
|
t('/preset --default 跟随 Host 默认'),
|
|
119
|
+
t('/permissionlist 按序号列出权限档位'),
|
|
120
|
+
t('/permission [序号或完整ID] 查看或切换当前会话权限'),
|
|
121
|
+
t('纯数字 ID:/permission id:<ID>'),
|
|
118
122
|
t('/stop 停止当前任务'),
|
|
119
123
|
t('/steer 补充指令 纠偏当前任务'),
|
|
120
124
|
t('/batch 开始批量输入(仅私聊,最多 10 条文字)'),
|
|
@@ -523,7 +527,7 @@ export class QqHarnessBridge {
|
|
|
523
527
|
text: commandText,
|
|
524
528
|
hasImages: hasQqImageAttachments(message),
|
|
525
529
|
hasFiles: hasQqFileAttachments(message),
|
|
526
|
-
...(explicitMenu || numericMenu ? { isCommand: true } : {}),
|
|
530
|
+
...(explicitMenu || numericMenu || isPermissionCommand(commandText) ? { isCommand: true } : {}),
|
|
527
531
|
})
|
|
528
532
|
: senderAllowed(message, this.#ownerUserOpenid)
|
|
529
533
|
? { allowed: true, reason: 'legacy-owner' }
|
|
@@ -594,6 +598,7 @@ export class QqHarnessBridge {
|
|
|
594
598
|
const commandRunner = explicitMenu || numericMenu
|
|
595
599
|
? () => this.#runMenuCommand(message, menuInput, key)
|
|
596
600
|
: isHistoryCommand(commandText) ? runHistoryCommand
|
|
601
|
+
: isPermissionCommand(commandText) ? runPermissionCommand
|
|
597
602
|
: hasQqFileAttachments(message) ? null : isControlCommand(commandText)
|
|
598
603
|
? runControlCommand
|
|
599
604
|
: (isModelCommand(commandText)
|
|
@@ -2002,6 +2002,11 @@ export function createBotWorkspaceScope(
|
|
|
2002
2002
|
models(...args) {
|
|
2003
2003
|
return invokeCurrentSession('getSessionModels', args, 'model listing');
|
|
2004
2004
|
},
|
|
2005
|
+
...(typeof target.getSessionPermissions === 'function' ? {
|
|
2006
|
+
permissions(...args) {
|
|
2007
|
+
return invokeCurrentSession('getSessionPermissions', args, 'permission listing');
|
|
2008
|
+
},
|
|
2009
|
+
} : {}),
|
|
2005
2010
|
readHistory(...args) {
|
|
2006
2011
|
return invokeCurrentSession('readSessionHistory', args, 'history read');
|
|
2007
2012
|
},
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const COMMAND_LIST_TTL_MS = 15 * 60_000;
|
|
2
|
+
export const COMMAND_LIST_MAX_ENTRIES = 256;
|
|
3
|
+
|
|
4
|
+
/** Short-lived lists scoped to a bot's state and conversation, never persisted. */
|
|
5
|
+
export function createCommandListSnapshots() {
|
|
6
|
+
const states = new WeakMap();
|
|
7
|
+
return {
|
|
8
|
+
save(state, key, value) {
|
|
9
|
+
if ((!state || typeof state !== 'object') && typeof state !== 'function') return;
|
|
10
|
+
let entries = states.get(state);
|
|
11
|
+
if (!entries) states.set(state, entries = new Map());
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [entryKey, entry] of entries) {
|
|
14
|
+
if (entry.expiresAt <= now) entries.delete(entryKey);
|
|
15
|
+
}
|
|
16
|
+
entries.delete(key);
|
|
17
|
+
entries.set(key, { value, expiresAt: now + COMMAND_LIST_TTL_MS });
|
|
18
|
+
while (entries.size > COMMAND_LIST_MAX_ENTRIES) entries.delete(entries.keys().next().value);
|
|
19
|
+
},
|
|
20
|
+
load(state, key) {
|
|
21
|
+
const entries = states.get(state);
|
|
22
|
+
const entry = entries?.get(key);
|
|
23
|
+
if (!entry) return null;
|
|
24
|
+
if (entry.expiresAt <= Date.now()) {
|
|
25
|
+
entries.delete(key);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
entries.delete(key);
|
|
29
|
+
entries.set(key, entry);
|
|
30
|
+
return entry.value;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1139,6 +1139,12 @@ export class HarnessClient {
|
|
|
1139
1139
|
return validateModelCatalog(value, 'session.models', { session: true });
|
|
1140
1140
|
}
|
|
1141
1141
|
|
|
1142
|
+
async getSessionPermissions(sessionId, options = {}) {
|
|
1143
|
+
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
1144
|
+
await this.ensureRunning(options);
|
|
1145
|
+
return this.rpc('session.permissions', { sessionId }, 30_000, options);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1142
1148
|
async selectSessionModel(sessionId, selection, options = {}) {
|
|
1143
1149
|
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
1144
1150
|
if (!validModelSelection(selection)) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'用法:/permission 查看当前权限;/permissionlist 列出档位;/permission <序号或完整ID> 切换;纯数字 ID 使用 /permission id:<ID>。': 'Usage: /permission to show current permissions; /permissionlist to list presets; /permission <number or full ID> to switch; use /permission id:<ID> for numeric IDs.',
|
|
3
|
+
'仅影响当前会话;沙箱与审批策略会同时按 Host 的档位定义更新。': 'Applies only to the current session; sandbox and approval policies change together according to the Host preset.',
|
|
4
|
+
'请先执行 /permissionlist,再按列表序号选择权限档位。': 'Run /permissionlist first, then select a permission preset by its number.',
|
|
5
|
+
'完全文件访问,不再请求审批。': 'Full file access without approval prompts.',
|
|
6
|
+
'可写工作区和允许的临时目录;扩大访问范围需要审批。': 'Write inside the workspace and permitted temporary directories; wider retries require approval.',
|
|
7
|
+
'当前聊天绑定的会话已不存在,请先绑定或建立会话。': 'The bound session no longer exists. Bind or start a session first.',
|
|
8
|
+
'当前 Host 暂不支持从机器人查看或切换权限档位。': 'This Host does not support viewing or switching permission presets from the bot.',
|
|
9
|
+
'未能确认权限切换结果,请执行 /permission 核对当前档位。': 'Could not confirm the permission change. Run /permission to check the current preset.',
|
|
10
|
+
'暂时无法获取权限档位,请稍后重试。': 'Could not read permission presets. Please try again later.',
|
|
11
|
+
'权限命令仅支持纯文字,请移除图片或文件后重试。': 'Permission commands require plain text. Remove images or files and try again.',
|
|
12
|
+
'当前聊天尚未绑定会话,请先发送普通消息建立会话,或使用 /session 绑定已有会话。': 'This chat has no bound session. Send a regular message to start one, or use /session to bind an existing session.',
|
|
13
|
+
'当前会话已变化,请重新执行 /permissionlist。': 'The current session changed. Run /permissionlist again.',
|
|
14
|
+
'当前会话权限:{id}': 'Current session permissions: {id}',
|
|
15
|
+
' ✓ 当前': ' ✓ Current',
|
|
16
|
+
'当前没有可用权限档位。': 'No permission presets are currently available.',
|
|
17
|
+
'切换:/permission <序号或完整ID>': 'Switch: /permission <number or full ID>',
|
|
18
|
+
'查看可用档位与序号:/permissionlist': 'Show available presets and numbers: /permissionlist',
|
|
19
|
+
'权限档位序号无效,请重新执行 /permissionlist。': 'Invalid permission preset number. Run /permissionlist again.',
|
|
20
|
+
'当前 Host 未注册 /permission 命令,请确认权限预设组件已启用。': 'This Host has not registered /permission. Check that permission presets are enabled.',
|
|
21
|
+
'该权限档位不存在或已不可用,请重新执行 /permissionlist。': 'That permission preset no longer exists or is unavailable. Run /permissionlist again.',
|
|
22
|
+
'权限切换失败。': 'Permission change failed.',
|
|
23
|
+
'当前会话权限已切换为:{id}': 'Current session permissions changed to: {id}',
|
|
24
|
+
'恢复工作区权限:/permission workspace-write(需 Host 支持该档位)': 'Restore workspace permissions: /permission workspace-write (if supported by the Host)',
|
|
25
|
+
'/permissionlist 按序号列出权限档位': '/permissionlist List numbered permission presets',
|
|
26
|
+
'/permission [序号或完整ID] 查看或切换当前会话权限': '/permission [number or full ID] Show or switch current session permissions',
|
|
27
|
+
'纯数字 ID:/permission id:<ID>': 'Numeric IDs: /permission id:<ID>',
|
|
28
|
+
'`/permissionlist` — 按序号列出权限档位': '`/permissionlist` — List numbered permission presets',
|
|
29
|
+
'`/permission [序号/ID]` — 查看或切换当前会话权限': '`/permission [number/ID]` — Show or switch current session permissions',
|
|
30
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import imageInput from './i18n-en/image-input.mjs';
|
|
2
|
+
import permission from './i18n-en/permission.mjs';
|
|
2
3
|
import diagnostics from './i18n-en/diagnostics.mjs';
|
|
3
4
|
// English translations for host-side user-facing text.
|
|
4
5
|
// Keys are the exact Chinese literals passed to t() in src/channels/**.
|
|
@@ -26,6 +27,7 @@ export const EN = Object.freeze(Object.assign(
|
|
|
26
27
|
{},
|
|
27
28
|
diagnostics,
|
|
28
29
|
imageInput,
|
|
30
|
+
permission,
|
|
29
31
|
sharedA,
|
|
30
32
|
sharedB,
|
|
31
33
|
sharedC,
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createCommandListSnapshots } from './command-list-snapshots.mjs';
|
|
2
|
+
import { t } from './i18n.mjs';
|
|
3
|
+
import { withSessionBindingLock } from './session-binding-lock.mjs';
|
|
4
|
+
import { splitWorkspaceCommandMessage } from './workspace-command.mjs';
|
|
5
|
+
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
6
|
+
|
|
7
|
+
const COMMAND = /^\/(?:permission|permissionlist)(?=$|\s)/iu;
|
|
8
|
+
const SNAPSHOTS = createCommandListSnapshots();
|
|
9
|
+
const USAGE = '用法:/permission 查看当前权限;/permissionlist 列出档位;/permission <序号或完整ID> 切换;纯数字 ID 使用 /permission id:<ID>。';
|
|
10
|
+
const SCOPE = '仅影响当前会话;沙箱与审批策略会同时按 Host 的档位定义更新。';
|
|
11
|
+
const REFRESH = '请先执行 /permissionlist,再按列表序号选择权限档位。';
|
|
12
|
+
const validId = (id) => typeof id === 'string' && /^[^\s\p{Cc}\p{Cf}]+$/u.test(id);
|
|
13
|
+
const display = (value) => typeof value === 'string'
|
|
14
|
+
? value.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu, ' ').trim() : '';
|
|
15
|
+
|
|
16
|
+
function result(message) {
|
|
17
|
+
return { handled: true, message, messages: splitWorkspaceCommandMessage(message) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function permissions(value) {
|
|
21
|
+
if (!value || !validId(value.currentValue) || !Array.isArray(value.options)
|
|
22
|
+
|| value.options.some((item) => !item || !validId(item.value))) {
|
|
23
|
+
throw new TypeError('Harness returned invalid permissions');
|
|
24
|
+
}
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
return {
|
|
27
|
+
currentValue: value.currentValue,
|
|
28
|
+
options: value.options.filter((item) => {
|
|
29
|
+
if (item.value === 'custom' || seen.has(item.value)) return false;
|
|
30
|
+
seen.add(item.value);
|
|
31
|
+
return true;
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function description(item) {
|
|
37
|
+
// Translate known Host copy, not an assumed ID -> policy mapping: presets are configurable.
|
|
38
|
+
const text = display(item.description);
|
|
39
|
+
if (text === 'Full file access without approval prompts.') {
|
|
40
|
+
return t('完全文件访问,不再请求审批。');
|
|
41
|
+
}
|
|
42
|
+
if (text === 'Write inside the workspace and permitted temporary directories; wider retries require approval.') {
|
|
43
|
+
return t('可写工作区和允许的临时目录;扩大访问范围需要审批。');
|
|
44
|
+
}
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function errorMessage(error, writing) {
|
|
49
|
+
const code = error?.code ?? error?.failure?.code;
|
|
50
|
+
if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
|
|
51
|
+
return t('工作区或机器人状态已发生变化,请重试。');
|
|
52
|
+
}
|
|
53
|
+
if (['session-not-found', 'session/not-found', 'agent-not-found'].includes(code)) {
|
|
54
|
+
return t('当前聊天绑定的会话已不存在,请先绑定或建立会话。');
|
|
55
|
+
}
|
|
56
|
+
if (['commands-unavailable', 'permissions-unavailable', 'harness-api-not-found',
|
|
57
|
+
'endpoint-not-found', 'gateway/endpoint-not-found'].includes(code)) {
|
|
58
|
+
return t('当前 Host 暂不支持从机器人查看或切换权限档位。');
|
|
59
|
+
}
|
|
60
|
+
return writing
|
|
61
|
+
? t('未能确认权限切换结果,请执行 /permission 核对当前档位。')
|
|
62
|
+
: t('暂时无法获取权限档位,请稍后重试。');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isPermissionCommand(text) {
|
|
66
|
+
return typeof text === 'string' && COMMAND.test(text.trim());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runPermissionCommand(text, harness, state, key, options = {}) {
|
|
70
|
+
if (!isPermissionCommand(text)) return null;
|
|
71
|
+
if (options.hasImages || options.hasFiles) {
|
|
72
|
+
return result(t('权限命令仅支持纯文字,请移除图片或文件后重试。'));
|
|
73
|
+
}
|
|
74
|
+
const match = /^\/(permission|permissionlist)(?:[ \t]+([^\s]+))?[ \t]*$/iu.exec(text.trim());
|
|
75
|
+
if (!match || (match[1].toLowerCase() === 'permissionlist' && match[2])) return result(t(USAGE));
|
|
76
|
+
const list = match[1].toLowerCase() === 'permissionlist';
|
|
77
|
+
const requested = match[2];
|
|
78
|
+
if (requested && (!validId(requested) || requested.startsWith('--'))) return result(t(USAGE));
|
|
79
|
+
const signal = options.signal
|
|
80
|
+
? AbortSignal.any([options.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000);
|
|
81
|
+
let writing = false;
|
|
82
|
+
try {
|
|
83
|
+
return await withSessionBindingLock(state, key, async () => {
|
|
84
|
+
signal.throwIfAborted();
|
|
85
|
+
const sessionId = state.sessionFor?.(key);
|
|
86
|
+
if (!sessionId) return result(t('当前聊天尚未绑定会话,请先发送普通消息建立会话,或使用 /session 绑定已有会话。'));
|
|
87
|
+
const session = typeof harness.workspaceSession === 'function'
|
|
88
|
+
? harness.workspaceSession(sessionId, key)
|
|
89
|
+
: {
|
|
90
|
+
permissions: harness.getSessionPermissions && ((opts) => harness.getSessionPermissions(sessionId, opts)),
|
|
91
|
+
executeCommand: harness.executeCommand && ((line, opts) => harness.executeCommand(sessionId, line, opts)),
|
|
92
|
+
};
|
|
93
|
+
if (!requested) {
|
|
94
|
+
if (typeof session?.permissions !== 'function') return result(t('当前 Host 暂不支持从机器人查看或切换权限档位。'));
|
|
95
|
+
const current = permissions(await session.permissions({ signal }));
|
|
96
|
+
if (state.sessionFor(key) !== sessionId) return result(t('当前会话已变化,请重新执行 /permissionlist。'));
|
|
97
|
+
const lines = [t('当前会话权限:{id}', { id: current.currentValue }), ''];
|
|
98
|
+
if (list) {
|
|
99
|
+
SNAPSHOTS.save(state, key, { sessionId, ids: current.options.map((item) => item.value) });
|
|
100
|
+
for (const [index, item] of current.options.entries()) {
|
|
101
|
+
const label = display(item.name);
|
|
102
|
+
const detail = description(item);
|
|
103
|
+
lines.push(`${index + 1}. ${item.value}${label && label !== item.value ? `(${label})` : ''}${item.value === current.currentValue ? t(' ✓ 当前') : ''}${detail ? ` — ${detail}` : ''}`);
|
|
104
|
+
}
|
|
105
|
+
if (!current.options.length) lines.push(t('当前没有可用权限档位。'));
|
|
106
|
+
lines.push('', t('切换:/permission <序号或完整ID>'));
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(t('查看可用档位与序号:/permissionlist'));
|
|
109
|
+
}
|
|
110
|
+
lines.push(t(SCOPE));
|
|
111
|
+
return result(lines.join('\n'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let selected = requested;
|
|
115
|
+
const numericId = /^id:(\d+)$/iu.exec(requested);
|
|
116
|
+
if (numericId) selected = numericId[1];
|
|
117
|
+
else if (/^\d+$/u.test(requested)) {
|
|
118
|
+
const snapshot = SNAPSHOTS.load(state, key);
|
|
119
|
+
if (!snapshot || snapshot.sessionId !== sessionId) return result(t(REFRESH));
|
|
120
|
+
const index = Number(requested);
|
|
121
|
+
if (!Number.isSafeInteger(index) || index < 1 || !snapshot.ids[index - 1]) {
|
|
122
|
+
return result(t('权限档位序号无效,请重新执行 /permissionlist。'));
|
|
123
|
+
}
|
|
124
|
+
selected = snapshot.ids[index - 1];
|
|
125
|
+
}
|
|
126
|
+
if (selected === 'custom' || /^id:/iu.test(selected)) return result(t(USAGE));
|
|
127
|
+
if (typeof session?.executeCommand !== 'function') return result(t('当前 Host 暂不支持从机器人查看或切换权限档位。'));
|
|
128
|
+
signal.throwIfAborted();
|
|
129
|
+
writing = true;
|
|
130
|
+
const execution = await session.executeCommand(`/permission ${selected}`, { signal });
|
|
131
|
+
if (execution === undefined) return result(t('当前 Host 未注册 /permission 命令,请确认权限预设组件已启用。'));
|
|
132
|
+
const outcome = execution?.result;
|
|
133
|
+
if (!outcome || !['success', 'error'].includes(outcome.kind)
|
|
134
|
+
|| (outcome.text !== undefined && typeof outcome.text !== 'string')) throw new TypeError('Invalid permission command result');
|
|
135
|
+
if (outcome.kind === 'error') {
|
|
136
|
+
if (outcome.text?.startsWith('unknown preset ')) {
|
|
137
|
+
return result(t('该权限档位不存在或已不可用,请重新执行 /permissionlist。'));
|
|
138
|
+
}
|
|
139
|
+
return result([t('权限切换失败。'), display(outcome.text)].filter(Boolean).join('\n'));
|
|
140
|
+
}
|
|
141
|
+
return result([
|
|
142
|
+
t('当前会话权限已切换为:{id}', { id: selected }), '', t(SCOPE),
|
|
143
|
+
...(selected === 'danger-full-access' ? [t('恢复工作区权限:/permission workspace-write(需 Host 支持该档位)')] : []),
|
|
144
|
+
].join('\n'));
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return result(errorMessage(error, writing));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createCommandListSnapshots, COMMAND_LIST_TTL_MS, COMMAND_LIST_MAX_ENTRIES } from './command-list-snapshots.mjs';
|
|
1
2
|
import {
|
|
2
3
|
normalizeAgentPresetCatalog,
|
|
3
4
|
normalizeAgentPresetId,
|
|
@@ -19,9 +20,9 @@ const PRESET_USAGE = [
|
|
|
19
20
|
'/preset --default 跟随 Host 默认',
|
|
20
21
|
].join('\n');
|
|
21
22
|
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
|
22
|
-
const LIST_SNAPSHOTS =
|
|
23
|
-
export const PRESET_LIST_SNAPSHOT_TTL_MS =
|
|
24
|
-
export const PRESET_LIST_SNAPSHOT_MAX_ENTRIES =
|
|
23
|
+
const LIST_SNAPSHOTS = createCommandListSnapshots();
|
|
24
|
+
export const PRESET_LIST_SNAPSHOT_TTL_MS = COMMAND_LIST_TTL_MS;
|
|
25
|
+
export const PRESET_LIST_SNAPSHOT_MAX_ENTRIES = COMMAND_LIST_MAX_ENTRIES;
|
|
25
26
|
|
|
26
27
|
function commandResult(message) {
|
|
27
28
|
return {
|
|
@@ -140,59 +141,13 @@ function formatUpdated(settings) {
|
|
|
140
141
|
].join('\n');
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
function stateSnapshots(state, { create = false } = {}) {
|
|
144
|
-
if ((typeof state !== 'object' || state === null) && typeof state !== 'function') return null;
|
|
145
|
-
let snapshots = LIST_SNAPSHOTS.get(state);
|
|
146
|
-
if (!snapshots && create) {
|
|
147
|
-
snapshots = new Map();
|
|
148
|
-
LIST_SNAPSHOTS.set(state, snapshots);
|
|
149
|
-
}
|
|
150
|
-
return snapshots ?? null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function pruneExpiredSnapshots(snapshots, now) {
|
|
154
|
-
for (const [snapshotKey, snapshot] of snapshots) {
|
|
155
|
-
if (snapshot.expiresAt <= now) snapshots.delete(snapshotKey);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function saveSnapshot(state, key, items) {
|
|
160
|
-
const snapshots = stateSnapshots(state, { create: true });
|
|
161
|
-
if (!snapshots) return;
|
|
162
|
-
const now = Date.now();
|
|
163
|
-
pruneExpiredSnapshots(snapshots, now);
|
|
164
|
-
snapshots.delete(key);
|
|
165
|
-
snapshots.set(key, {
|
|
166
|
-
expiresAt: now + PRESET_LIST_SNAPSHOT_TTL_MS,
|
|
167
|
-
ids: items.map((item) => item.id),
|
|
168
|
-
});
|
|
169
|
-
while (snapshots.size > PRESET_LIST_SNAPSHOT_MAX_ENTRIES) {
|
|
170
|
-
const oldest = snapshots.keys().next();
|
|
171
|
-
if (oldest.done) break;
|
|
172
|
-
snapshots.delete(oldest.value);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function loadSnapshot(state, key) {
|
|
177
|
-
const snapshots = stateSnapshots(state);
|
|
178
|
-
const snapshot = snapshots?.get(key);
|
|
179
|
-
if (!snapshots || !snapshot) return null;
|
|
180
|
-
if (snapshot.expiresAt <= Date.now()) {
|
|
181
|
-
snapshots.delete(key);
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
snapshots.delete(key);
|
|
185
|
-
snapshots.set(key, snapshot);
|
|
186
|
-
return snapshot.ids;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
144
|
function presetFromSnapshot(state, key, requested) {
|
|
190
145
|
if (!/^\d+$/u.test(requested)) return { numeric: false, id: null };
|
|
191
146
|
const index = Number(requested);
|
|
192
147
|
if (!Number.isSafeInteger(index) || index < 1) {
|
|
193
148
|
return { numeric: true, error: t('Agent Preset 序号无效,请先执行 /presetlist。') };
|
|
194
149
|
}
|
|
195
|
-
const snapshot =
|
|
150
|
+
const snapshot = LIST_SNAPSHOTS.load(state, key);
|
|
196
151
|
if (!snapshot) {
|
|
197
152
|
return { numeric: true, error: t('请先执行 /presetlist,再按列表序号选择 Agent Preset。') };
|
|
198
153
|
}
|
|
@@ -260,7 +215,7 @@ export async function runPresetCommand(text, harness, state, key, options = {})
|
|
|
260
215
|
if (!/^\/(?:presetlist|presets)[ \t]*$/iu.test(command)) return commandResult(t(PRESET_LIST_USAGE));
|
|
261
216
|
try {
|
|
262
217
|
const current = await settings(harness, requestOptions);
|
|
263
|
-
|
|
218
|
+
LIST_SNAPSHOTS.save(state, key, current.agentPresetCatalog.items.map((item) => item.id));
|
|
264
219
|
return commandResult(formatList(current));
|
|
265
220
|
} catch (error) {
|
|
266
221
|
return commandResult(presetErrorMessage(error, 'list'));
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
2
|
-
import { randomInt } from 'node:crypto';
|
|
3
2
|
|
|
4
3
|
import { createEditableMessageStream } from '../shared/editable-message-stream.mjs';
|
|
5
4
|
import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
|
|
@@ -357,6 +356,11 @@ function telegramMessageId(value) {
|
|
|
357
356
|
return Number.isSafeInteger(value?.message_id) ? String(value.message_id) : null;
|
|
358
357
|
}
|
|
359
358
|
|
|
359
|
+
function telegramMessageUnchanged(error) {
|
|
360
|
+
return error?.providerCode === 400
|
|
361
|
+
&& /^(?:Bad Request: )?message is not modified(?::|$)/iu.test(error?.message ?? '');
|
|
362
|
+
}
|
|
363
|
+
|
|
360
364
|
function telegramFailure(error) {
|
|
361
365
|
const providerCode = Number(error?.providerCode);
|
|
362
366
|
const status = Number(error?.status);
|
|
@@ -687,6 +691,9 @@ export class TelegramBotClient {
|
|
|
687
691
|
messageId: placeholderMessageId,
|
|
688
692
|
text: chunks[0],
|
|
689
693
|
signal: this.#signal,
|
|
694
|
+
}).catch((error) => {
|
|
695
|
+
// The final answer may already be visible from the last progress edit.
|
|
696
|
+
if (!telegramMessageUnchanged(error)) throw error;
|
|
690
697
|
});
|
|
691
698
|
firstUnsent = 1;
|
|
692
699
|
} catch (error) {
|
|
@@ -814,6 +821,8 @@ export class TelegramBotClient {
|
|
|
814
821
|
messageId,
|
|
815
822
|
richMessage: { markdown: chunks[0].markdown },
|
|
816
823
|
signal: this.#signal,
|
|
824
|
+
}).catch((error) => {
|
|
825
|
+
if (!telegramMessageUnchanged(error)) throw error;
|
|
817
826
|
});
|
|
818
827
|
} catch (error) {
|
|
819
828
|
const failure = telegramFailure(error);
|
|
@@ -869,31 +878,8 @@ export class TelegramBotClient {
|
|
|
869
878
|
}
|
|
870
879
|
|
|
871
880
|
async openDeliveryStream(target) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const updateDraft = async (block) => {
|
|
875
|
-
const richMessage = { markdown: toTelegramRichMarkdown(block.text) };
|
|
876
|
-
await this.#api.sendRichMessageDraft({
|
|
877
|
-
chatId: target.chatId,
|
|
878
|
-
draftId,
|
|
879
|
-
richMessage,
|
|
880
|
-
messageThreadId: target.messageThreadId,
|
|
881
|
-
signal: this.#signal,
|
|
882
|
-
});
|
|
883
|
-
return deliveryResult('telegram-rich-draft', []);
|
|
884
|
-
};
|
|
885
|
-
const stream = new TelegramDeliveryStream({
|
|
886
|
-
update: updateDraft,
|
|
887
|
-
finish: (block) => this.#sendRich(target, block),
|
|
888
|
-
fail: (block) => this.#sendPlain(target, block.text),
|
|
889
|
-
presentation: 'telegram-rich-draft',
|
|
890
|
-
keepalive: true,
|
|
891
|
-
logger: this.#logger,
|
|
892
|
-
});
|
|
893
|
-
await stream.update(createTextDeliveryBlock(t('正在处理…'), 'plain'));
|
|
894
|
-
return stream;
|
|
895
|
-
}
|
|
896
|
-
|
|
881
|
+
// Real messages keep the Android composer available for /stop and /steer;
|
|
882
|
+
// private-chat Rich Drafts can block sending for the whole turn (#276).
|
|
897
883
|
const placeholder = await this.#api.sendMessage({
|
|
898
884
|
chatId: target.chatId,
|
|
899
885
|
text: t('正在处理…'),
|