@xmanrui/dsh-im 4.33.0 → 4.34.1
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 +20 -4
- package/lib/index.js +294 -291
- package/package.json +1 -1
- package/plugin-src/client/access-policy-settings.js +18 -3
- package/plugin-src/client/i18n.js +5 -0
- package/plugin-src/host/channels/feishu/production.mjs +11 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/modern-harness-api.mjs +22 -0
- package/src/channels/discord/config-store.mjs +21 -1
- package/src/channels/discord/discord-api.mjs +32 -0
- package/src/channels/discord/discord-controller.mjs +22 -1
- package/src/channels/feishu/bridge.mjs +23 -1
- 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 +9 -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/feishu.mjs +2 -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
|
@@ -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('正在处理…'),
|