@xmanrui/dsh-im 4.20.2 → 4.21.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 +21 -9
- package/README.md +21 -9
- package/lib/client.js +116 -21
- package/lib/index.js +288 -287
- package/package.json +17 -1
- package/plugin-src/client/channel-logos.js +18 -5
- package/plugin-src/client/channels/imessage/styles.js +1 -1
- package/plugin-src/client/channels/slack/styles.js +1 -1
- package/plugin-src/client/channels/weixin/connection-error.js +4 -1
- package/plugin-src/client/i18n.js +8 -0
- package/plugin-src/client/model-setting.js +4 -2
- package/plugin-src/client/session-channel-logos.js +1 -2
- package/plugin-src/client/styles.js +3 -2
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/qq/rpc.mjs +2 -1
- package/plugin-src/host/index.mjs +7 -0
- package/plugin-src/host/injected-context.mjs +104 -0
- package/plugin-src/host/modern-harness-api.mjs +91 -3
- package/scripts/verify-model-setting.mjs +4 -1
- package/scripts/verify-package.mjs +3 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +105 -27
- package/src/channels/discord/discord-runtime.mjs +4 -1
- package/src/channels/feishu/bridge.mjs +44 -3
- package/src/channels/feishu/feishu-channel.mjs +1 -1
- package/src/channels/qq/qq-bridge.mjs +36 -3
- package/src/channels/qq/qq-controller.mjs +11 -5
- package/src/channels/qq/state-error.mjs +17 -0
- package/src/channels/qq/state-store.mjs +35 -9
- package/src/channels/shared/batch-input.mjs +22 -2
- package/src/channels/shared/bot-workspace-store.mjs +46 -26
- package/src/channels/shared/config-read-error.mjs +24 -0
- package/src/channels/shared/context-enhancement.mjs +40 -3
- package/src/channels/shared/control-command.mjs +8 -1
- package/src/channels/shared/harness-client.mjs +20 -12
- package/src/channels/shared/harness-question.mjs +10 -2
- package/src/channels/shared/i18n-en/dingtalk.mjs +1 -0
- package/src/channels/shared/i18n-en/qq.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +11 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
- package/src/channels/shared/i18n-en/weixin.mjs +2 -0
- package/src/channels/shared/im-source-guidance.mjs +65 -0
- package/src/channels/shared/injected-context.mjs +362 -0
- package/src/channels/shared/semantic/artifact.mjs +4 -4
- package/src/channels/shared/semantic/reply-reference.mjs +2 -1
- package/src/channels/shared/text-harness-bridge.mjs +221 -5
- package/src/channels/shared/token-config-store.mjs +23 -8
- package/src/channels/shared/workspace-session.mjs +16 -1
- package/src/channels/slack/slack-runtime.mjs +3 -1
- package/src/channels/telegram/telegram-api.mjs +62 -2
- package/src/channels/telegram/telegram-bridge.mjs +66 -1
- package/src/channels/telegram/telegram-rich-message.mjs +6 -4
- package/src/channels/telegram/telegram-runtime.mjs +128 -6
- package/src/channels/wecom/wecom-bridge.mjs +15 -1
- package/src/channels/wecom-app/config-store.mjs +3 -1
- package/src/channels/wecom-app/wecom-app-bridge.mjs +12 -1
- package/src/channels/weixin/config-store.mjs +24 -15
- package/src/channels/weixin/connection-error.en.mjs +21 -0
- package/src/channels/weixin/connection-error.mjs +21 -6
- package/src/channels/weixin/diagnostic-details.mjs +24 -1
- package/src/channels/weixin/weixin-api.mjs +52 -13
- package/src/channels/weixin/weixin-bridge.mjs +14 -1
|
@@ -32,11 +32,16 @@ import {
|
|
|
32
32
|
validateModelSelection,
|
|
33
33
|
} from './model-setting.mjs';
|
|
34
34
|
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
|
35
|
+
import { configValidationError, withConfigResource } from './config-read-error.mjs';
|
|
35
36
|
|
|
36
37
|
const DELIVERY_DOCUMENT_VERSION = 2;
|
|
37
38
|
export const CURRENT_DOCUMENT_VERSION = 3;
|
|
38
39
|
const EMPTY_DOCUMENT = Object.freeze({ version: 1, workspaces: Object.freeze({}) });
|
|
39
40
|
|
|
41
|
+
function invalidWorkspaceConfig(field, issue) {
|
|
42
|
+
throw configValidationError('dsh-im workspace config is invalid', field, issue);
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
function workspaceSessionStale(message) {
|
|
41
46
|
const error = new Error(message);
|
|
42
47
|
error.code = WORKSPACE_SESSION_STALE;
|
|
@@ -185,13 +190,17 @@ function sameDeliveryRoute(left, right) {
|
|
|
185
190
|
function normalizeDeliveryTargets(value, { version } = {}) {
|
|
186
191
|
const deliveryTargets = Object.create(null);
|
|
187
192
|
if (value === undefined) return deliveryTargets;
|
|
188
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return
|
|
193
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return invalidWorkspaceConfig('deliveryTargets', 'expected-object');
|
|
194
|
+
let field = 'deliveryTargets';
|
|
189
195
|
try {
|
|
190
|
-
for (const [botId, targets] of Object.entries(value)) {
|
|
196
|
+
for (const [botIndex, [botId, targets]] of Object.entries(value).entries()) {
|
|
197
|
+
field = `deliveryTargets[${botIndex}].key`;
|
|
191
198
|
botIdOf(botId);
|
|
192
|
-
|
|
199
|
+
field = `deliveryTargets[${botIndex}].targets`;
|
|
200
|
+
if (!targets || typeof targets !== 'object' || Array.isArray(targets)) throw new TypeError('Invalid targets');
|
|
193
201
|
const normalizedTargets = Object.create(null);
|
|
194
|
-
for (const [targetId, target] of Object.entries(targets)) {
|
|
202
|
+
for (const [targetIndex, [targetId, target]] of Object.entries(targets).entries()) {
|
|
203
|
+
field = `deliveryTargets[${botIndex}].targets[${targetIndex}]`;
|
|
195
204
|
// Backward compatibility: some released builds persisted the target id
|
|
196
205
|
// inside the stored object as well. Accept a redundant targetId that
|
|
197
206
|
// matches the map key when loading a stored document; a mismatch stays
|
|
@@ -215,7 +224,7 @@ function normalizeDeliveryTargets(value, { version } = {}) {
|
|
|
215
224
|
deliveryTargets[botId] = normalizedTargets;
|
|
216
225
|
}
|
|
217
226
|
} catch {
|
|
218
|
-
return
|
|
227
|
+
return invalidWorkspaceConfig(field, 'invalid-delivery-target');
|
|
219
228
|
}
|
|
220
229
|
return deliveryTargets;
|
|
221
230
|
}
|
|
@@ -241,44 +250,44 @@ function normalizeAccessPolicies(value, workspaces) {
|
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
function normalizeDocument(value) {
|
|
244
|
-
if (!value ||
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return invalidWorkspaceConfig('$', 'expected-object');
|
|
254
|
+
if (![1, DELIVERY_DOCUMENT_VERSION, CURRENT_DOCUMENT_VERSION].includes(value.version)) return invalidWorkspaceConfig('version', 'unsupported-version');
|
|
255
|
+
if (!value.workspaces || typeof value.workspaces !== 'object' || Array.isArray(value.workspaces)) return invalidWorkspaceConfig('workspaces', 'expected-object');
|
|
247
256
|
const workspaces = {};
|
|
248
|
-
for (const [botId, workspace] of Object.entries(value.workspaces)) {
|
|
249
|
-
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)
|
|
250
|
-
|
|
257
|
+
for (const [index, [botId, workspace]] of Object.entries(value.workspaces).entries()) {
|
|
258
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return invalidWorkspaceConfig(`workspaces[${index}].key`, 'invalid-identifier');
|
|
259
|
+
if (typeof workspace !== 'string' || !isAbsolute(workspace)) return invalidWorkspaceConfig(`workspaces[${index}].value`, 'invalid-workspace-path');
|
|
251
260
|
workspaces[botId] = resolve(workspace);
|
|
252
261
|
}
|
|
253
262
|
const conversationWorkspaces = normalizeConversationWorkspaces(value.conversationWorkspaces);
|
|
254
263
|
let agentPresets = {};
|
|
255
264
|
if (value.agentPresets !== undefined) {
|
|
256
265
|
if (!value.agentPresets || typeof value.agentPresets !== 'object'
|
|
257
|
-
|| Array.isArray(value.agentPresets)) return
|
|
258
|
-
for (const [botId, agentPreset] of Object.entries(value.agentPresets)) {
|
|
259
|
-
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return
|
|
266
|
+
|| Array.isArray(value.agentPresets)) return invalidWorkspaceConfig('agentPresets', 'expected-object');
|
|
267
|
+
for (const [index, [botId, agentPreset]] of Object.entries(value.agentPresets).entries()) {
|
|
268
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return invalidWorkspaceConfig(`agentPresets[${index}].key`, 'invalid-identifier');
|
|
260
269
|
try {
|
|
261
270
|
const normalized = validateAgentPresetId(agentPreset);
|
|
262
|
-
if (!normalized) return
|
|
271
|
+
if (!normalized) return invalidWorkspaceConfig(`agentPresets[${index}].value`, 'invalid-agent-preset');
|
|
263
272
|
agentPresets[botId] = normalized;
|
|
264
273
|
} catch {
|
|
265
|
-
return
|
|
274
|
+
return invalidWorkspaceConfig(`agentPresets[${index}].value`, 'invalid-agent-preset');
|
|
266
275
|
}
|
|
267
276
|
}
|
|
268
277
|
}
|
|
269
278
|
const models = {};
|
|
270
279
|
if (value.models !== undefined) {
|
|
271
280
|
if (!value.models || typeof value.models !== 'object' || Array.isArray(value.models)) {
|
|
272
|
-
return
|
|
281
|
+
return invalidWorkspaceConfig('models', 'expected-object');
|
|
273
282
|
}
|
|
274
|
-
for (const [botId, model] of Object.entries(value.models)) {
|
|
275
|
-
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return
|
|
283
|
+
for (const [index, [botId, model]] of Object.entries(value.models).entries()) {
|
|
284
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return invalidWorkspaceConfig(`models[${index}].key`, 'invalid-identifier');
|
|
276
285
|
try {
|
|
277
286
|
const normalized = validateModelSelection(model);
|
|
278
|
-
if (!normalized) return
|
|
287
|
+
if (!normalized) return invalidWorkspaceConfig(`models[${index}].value`, 'invalid-model-selection');
|
|
279
288
|
models[botId] = normalized;
|
|
280
289
|
} catch {
|
|
281
|
-
return
|
|
290
|
+
return invalidWorkspaceConfig(`models[${index}].value`, 'invalid-model-selection');
|
|
282
291
|
}
|
|
283
292
|
}
|
|
284
293
|
}
|
|
@@ -292,9 +301,8 @@ function normalizeDocument(value) {
|
|
|
292
301
|
}
|
|
293
302
|
}
|
|
294
303
|
}
|
|
295
|
-
if (value.version === 1 && value.deliveryTargets !== undefined) return
|
|
304
|
+
if (value.version === 1 && value.deliveryTargets !== undefined) return invalidWorkspaceConfig('deliveryTargets', 'unexpected-field');
|
|
296
305
|
const deliveryTargets = normalizeDeliveryTargets(value.deliveryTargets, { version: value.version });
|
|
297
|
-
if (!deliveryTargets) return null;
|
|
298
306
|
const aliases = Object.create(null);
|
|
299
307
|
if (value.aliases && typeof value.aliases === 'object' && !Array.isArray(value.aliases)) {
|
|
300
308
|
for (const [botId, alias] of Object.entries(value.aliases)) {
|
|
@@ -421,7 +429,6 @@ export class BotWorkspaceStore {
|
|
|
421
429
|
async load() {
|
|
422
430
|
try {
|
|
423
431
|
const normalized = normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
|
|
424
|
-
if (!normalized) throw new Error('dsh-im workspace config is invalid');
|
|
425
432
|
this.#version = normalized.version;
|
|
426
433
|
this.#workspaces = normalized.workspaces;
|
|
427
434
|
this.#agentPresets = normalized.agentPresets;
|
|
@@ -432,7 +439,7 @@ export class BotWorkspaceStore {
|
|
|
432
439
|
this.#accessPolicies = normalized.accessPolicies;
|
|
433
440
|
this.#conversationWorkspaces = normalized.conversationWorkspaces;
|
|
434
441
|
} catch (error) {
|
|
435
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
442
|
+
if (error?.code !== 'ENOENT') throw withConfigResource(error, 'workspace-config');
|
|
436
443
|
this.#version = 1;
|
|
437
444
|
this.#workspaces = {};
|
|
438
445
|
this.#agentPresets = {};
|
|
@@ -1371,6 +1378,18 @@ export class BotWorkspaceStore {
|
|
|
1371
1378
|
await this.#persist();
|
|
1372
1379
|
return;
|
|
1373
1380
|
}
|
|
1381
|
+
try {
|
|
1382
|
+
const parent = await stat(dirname(this.#path));
|
|
1383
|
+
if (!parent.isDirectory()) {
|
|
1384
|
+
const error = new Error('workspace config parent is not a directory');
|
|
1385
|
+
error.code = 'ENOTDIR';
|
|
1386
|
+
throw error;
|
|
1387
|
+
}
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1390
|
+
this.#dirtyRemovals.clear();
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1374
1393
|
try {
|
|
1375
1394
|
await unlink(this.#path);
|
|
1376
1395
|
this.#dirtyRemovals.clear();
|
|
@@ -2277,7 +2296,8 @@ export function createWorkspaceAwareController(controller, {
|
|
|
2277
2296
|
await state.clearSessions();
|
|
2278
2297
|
} catch (error) {
|
|
2279
2298
|
console.warn(
|
|
2280
|
-
|
|
2299
|
+
'[dsh-im] ignored session cleanup failure while deleting bot:',
|
|
2300
|
+
botId,
|
|
2281
2301
|
error?.message ?? error,
|
|
2282
2302
|
);
|
|
2283
2303
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Metadata belongs to the validation/read boundary, never to parser excerpts or
|
|
2
|
+
// arbitrary error properties. Keep the original exception type/message so other
|
|
3
|
+
// channels retain their existing startup classification.
|
|
4
|
+
const details = new WeakMap();
|
|
5
|
+
|
|
6
|
+
export function configValidationError(message, field, issue) {
|
|
7
|
+
const error = new Error(message);
|
|
8
|
+
details.set(error, { reason: 'invalid-config', field, issue });
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function withConfigResource(error, resource) {
|
|
13
|
+
if (error && typeof error === 'object') {
|
|
14
|
+
details.set(error, {
|
|
15
|
+
...details.get(error), resource,
|
|
16
|
+
...(error instanceof SyntaxError ? { reason: 'invalid-json' } : {}),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function configReadErrorDetails(error) {
|
|
23
|
+
return details.get(error);
|
|
24
|
+
}
|
|
@@ -4,6 +4,23 @@ export const CONTEXT_ENHANCEMENT_FIELDS = Object.freeze([
|
|
|
4
4
|
'chatId', 'threadId', 'botId',
|
|
5
5
|
]);
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Tag grammar of the injected context prefix. The producer here and the
|
|
9
|
+
* Host-side splitter in `injected-context.mjs` share these literals, so the
|
|
10
|
+
* prefix a channel writes can never drift from the parser that pairs it.
|
|
11
|
+
*/
|
|
12
|
+
export const INJECTED_CONTEXT_TAGS = Object.freeze({
|
|
13
|
+
sourceOpen: '<dsh_im_source>',
|
|
14
|
+
sourceClose: '</dsh_im_source>',
|
|
15
|
+
guidanceOpen: '<dsh_im_source_guidance>',
|
|
16
|
+
guidanceClose: '</dsh_im_source_guidance>',
|
|
17
|
+
replyOpen: '<dsh_im_reply_to>',
|
|
18
|
+
replyClose: '</dsh_im_reply_to>',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** Separator the producer joins prefix blocks with, and the splitter consumes. */
|
|
22
|
+
export const INJECTED_CONTEXT_SEPARATOR = '\n\n';
|
|
23
|
+
|
|
7
24
|
export const CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8_000;
|
|
8
25
|
export const CONTEXT_GROUP_GUIDANCE_EXAMPLE = `仅依据当前消息的 <dsh_im_source> 中实际提供的字段理解来源;没有提供的字段不要猜测或补全。
|
|
9
26
|
当前消息来自群聊,请使用严肃、克制、简洁的表达方式。`;
|
|
@@ -138,6 +155,26 @@ export function captureContextEnhancement(provider, conversationType) {
|
|
|
138
155
|
}
|
|
139
156
|
}
|
|
140
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Capture the enhancement one prompt replays, together with the source factory
|
|
160
|
+
* that fills its selected fields.
|
|
161
|
+
*
|
|
162
|
+
* Ordinary messages snapshot this when they are accepted, so a queued message
|
|
163
|
+
* keeps the settings it arrived under. A control command is never queued, so it
|
|
164
|
+
* captures at the moment it runs -- and it must, because the source fields of a
|
|
165
|
+
* steering instruction belong to whoever issued it, not to the message that
|
|
166
|
+
* opened the turn.
|
|
167
|
+
*
|
|
168
|
+
* @param provider - the bot's enhancement provider.
|
|
169
|
+
* @param conversationType - the inbound message's scope.
|
|
170
|
+
* @param source - factory for the currently selected source fields.
|
|
171
|
+
* @returns the enhancement to apply, or null when the scope is off.
|
|
172
|
+
*/
|
|
173
|
+
export function captureContextEnhancementSource(provider, conversationType, source) {
|
|
174
|
+
const snapshot = captureContextEnhancement(provider, conversationType);
|
|
175
|
+
return snapshot === null ? null : Object.freeze({ snapshot, source });
|
|
176
|
+
}
|
|
177
|
+
|
|
141
178
|
function sourceString(value, field) {
|
|
142
179
|
if (field === 'senderId' && (typeof value === 'bigint' || Number.isFinite(value))) {
|
|
143
180
|
value = String(value);
|
|
@@ -165,7 +202,7 @@ function sourceBlock(snapshot, sourceFactory) {
|
|
|
165
202
|
const json = JSON.stringify(projected).replace(/[<>&]/g, (character) => ({
|
|
166
203
|
'<': '\\u003c', '>': '\\u003e', '&': '\\u0026',
|
|
167
204
|
})[character]);
|
|
168
|
-
return
|
|
205
|
+
return `${INJECTED_CONTEXT_TAGS.sourceOpen}${json}${INJECTED_CONTEXT_TAGS.sourceClose}`;
|
|
169
206
|
}
|
|
170
207
|
|
|
171
208
|
function guidanceBlock(guidance) {
|
|
@@ -173,7 +210,7 @@ function guidanceBlock(guidance) {
|
|
|
173
210
|
const body = guidance.replace(/<\/?dsh_im_source_guidance\b[^>]*(?:>|$)/gi, (tag) => (
|
|
174
211
|
tag.replace(/</g, '<').replace(/>/g, '>')
|
|
175
212
|
));
|
|
176
|
-
return
|
|
213
|
+
return `${INJECTED_CONTEXT_TAGS.guidanceOpen}\n${body}\n${INJECTED_CONTEXT_TAGS.guidanceClose}`;
|
|
177
214
|
}
|
|
178
215
|
|
|
179
216
|
/** Add one text prefix; never inspect sources, format or copy content when off. */
|
|
@@ -183,7 +220,7 @@ export function enhanceContextContent(content, snapshot, sourceFactory) {
|
|
|
183
220
|
const blocks = [sourceBlock(snapshot, sourceFactory), guidanceBlock(snapshot.config.guidance)]
|
|
184
221
|
.filter(Boolean);
|
|
185
222
|
if (blocks.length === 0) return content;
|
|
186
|
-
const prefix = blocks.join(
|
|
223
|
+
const prefix = blocks.join(INJECTED_CONTEXT_SEPARATOR);
|
|
187
224
|
if (typeof content === 'string') return `${prefix}\n\n${content}`;
|
|
188
225
|
if (Array.isArray(content)) return [{ type: 'text', text: prefix }, ...content];
|
|
189
226
|
return content;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enhanceContextContent } from './context-enhancement.mjs';
|
|
1
2
|
import { t } from './i18n.mjs';
|
|
2
3
|
import manifest from '../../../package.json' with { type: 'json' };
|
|
3
4
|
|
|
@@ -41,6 +42,7 @@ export async function runControlCommand(text, harness, state, key, {
|
|
|
41
42
|
pendingInteraction = false,
|
|
42
43
|
control,
|
|
43
44
|
deferredDelivery,
|
|
45
|
+
enhancement,
|
|
44
46
|
} = {}) {
|
|
45
47
|
if (!isControlCommand(text)) return null;
|
|
46
48
|
const command = text.trim();
|
|
@@ -93,8 +95,13 @@ export async function runControlCommand(text, harness, state, key, {
|
|
|
93
95
|
if (typeof session.steerActiveTurn !== 'function') {
|
|
94
96
|
throw new TypeError('Harness session does not support steering active turns');
|
|
95
97
|
}
|
|
98
|
+
// A mid-turn correction carries the same provenance as the message that
|
|
99
|
+
// opened the turn, so a group member who steers is identified too.
|
|
100
|
+
const steering = enhancement
|
|
101
|
+
? enhanceContextContent(instruction, enhancement.snapshot, enhancement.source)
|
|
102
|
+
: instruction;
|
|
96
103
|
const steered = await session.steerActiveTurn(
|
|
97
|
-
|
|
104
|
+
steering,
|
|
98
105
|
control,
|
|
99
106
|
requestOptions(signal),
|
|
100
107
|
);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
imageFileSourcesFromContent,
|
|
14
14
|
isModelImageRejection,
|
|
15
15
|
} from './image-prompt.mjs';
|
|
16
|
+
import { imSourceGuidance } from './im-source-guidance.mjs';
|
|
16
17
|
import { outboundArtifactRegistry } from './semantic/artifact.mjs';
|
|
17
18
|
import { t } from './i18n.mjs';
|
|
18
19
|
import { watchHarnessMux } from './harness-mux.mjs';
|
|
@@ -893,7 +894,7 @@ export class HarnessClient {
|
|
|
893
894
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
894
895
|
});
|
|
895
896
|
this.#managedProcess.on('error', (error) => {
|
|
896
|
-
console.error(
|
|
897
|
+
console.error('[dsh-im] failed to start Harness:', this.#logPrefix, error.message);
|
|
897
898
|
});
|
|
898
899
|
}
|
|
899
900
|
|
|
@@ -1123,7 +1124,7 @@ export class HarnessClient {
|
|
|
1123
1124
|
});
|
|
1124
1125
|
} catch (error) {
|
|
1125
1126
|
if (signal.aborted) return;
|
|
1126
|
-
console.warn(
|
|
1127
|
+
console.warn('[dsh-im] Harness interaction stream disconnected:', this.#logPrefix, error.message);
|
|
1127
1128
|
}
|
|
1128
1129
|
if (signal.aborted) return;
|
|
1129
1130
|
try {
|
|
@@ -1462,7 +1463,7 @@ export class HarnessClient {
|
|
|
1462
1463
|
deliveredArtifactCount += 1;
|
|
1463
1464
|
} catch (error) {
|
|
1464
1465
|
outboundArtifactRegistry.release(artifact);
|
|
1465
|
-
console.warn(
|
|
1466
|
+
console.warn('[dsh-im] ignored an artifact handoff failure:', this.#logPrefix, error.message);
|
|
1466
1467
|
}
|
|
1467
1468
|
}
|
|
1468
1469
|
return deliveredArtifactCount;
|
|
@@ -1508,6 +1509,12 @@ export class HarnessClient {
|
|
|
1508
1509
|
if (!Array.isArray(content) || content.length === 0) {
|
|
1509
1510
|
throw new TypeError('Harness prompt content is required');
|
|
1510
1511
|
}
|
|
1512
|
+
// Publish the guidance the channel's own captured settings produced, so
|
|
1513
|
+
// the Host materializes it once per Session as prompt context instead of
|
|
1514
|
+
// per user message. It is passed in and never parsed back out of
|
|
1515
|
+
// `content`: the prompt also carries whatever the user typed, and a
|
|
1516
|
+
// message that merely looks like a guidance block is not configuration.
|
|
1517
|
+
imSourceGuidance.publish(sessionId, options.sourceGuidance);
|
|
1511
1518
|
const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1512
1519
|
const sendPrompt = (promptContent) => this.rpc('session.prompt', {
|
|
1513
1520
|
sessionId,
|
|
@@ -1534,7 +1541,8 @@ export class HarnessClient {
|
|
|
1534
1541
|
} catch (stagingError) {
|
|
1535
1542
|
if (signal?.aborted) throw signal.reason ?? stagingError;
|
|
1536
1543
|
console.warn(
|
|
1537
|
-
|
|
1544
|
+
'[dsh-im] unable to restage rejected images as workspace files:',
|
|
1545
|
+
this.#logPrefix,
|
|
1538
1546
|
stagingError?.message ?? String(stagingError),
|
|
1539
1547
|
);
|
|
1540
1548
|
throw error;
|
|
@@ -1586,7 +1594,7 @@ export class HarnessClient {
|
|
|
1586
1594
|
try {
|
|
1587
1595
|
await onUpdate(update);
|
|
1588
1596
|
} catch (error) {
|
|
1589
|
-
console.warn(
|
|
1597
|
+
console.warn('[dsh-im] ignored a progress update failure:', this.#logPrefix, error.message);
|
|
1590
1598
|
}
|
|
1591
1599
|
}
|
|
1592
1600
|
}
|
|
@@ -1643,7 +1651,7 @@ export class HarnessClient {
|
|
|
1643
1651
|
try {
|
|
1644
1652
|
await staged?.cleanup?.();
|
|
1645
1653
|
} catch (error) {
|
|
1646
|
-
console.warn(
|
|
1654
|
+
console.warn('[dsh-im] unable to clean inbound files:', this.#logPrefix, error.message);
|
|
1647
1655
|
}
|
|
1648
1656
|
}
|
|
1649
1657
|
}
|
|
@@ -1677,7 +1685,7 @@ export class HarnessClient {
|
|
|
1677
1685
|
try {
|
|
1678
1686
|
onOpen?.();
|
|
1679
1687
|
} catch (error) {
|
|
1680
|
-
console.warn(
|
|
1688
|
+
console.warn('[dsh-im] ignored an interaction open callback failure:', this.#logPrefix, error.message);
|
|
1681
1689
|
}
|
|
1682
1690
|
if (ownership) {
|
|
1683
1691
|
void this.#refreshInteractionOwnerships(sessionId, signal).then(() => {
|
|
@@ -1769,7 +1777,7 @@ export class HarnessClient {
|
|
|
1769
1777
|
if (!ownershipReady) bufferedEnvelopes.push(envelope);
|
|
1770
1778
|
else processEnvelope(envelope);
|
|
1771
1779
|
} catch (error) {
|
|
1772
|
-
console.warn(
|
|
1780
|
+
console.warn('[dsh-im] ignored a malformed Harness interaction frame:', this.#logPrefix, error.message);
|
|
1773
1781
|
}
|
|
1774
1782
|
};
|
|
1775
1783
|
try {
|
|
@@ -1806,7 +1814,7 @@ export class HarnessClient {
|
|
|
1806
1814
|
try {
|
|
1807
1815
|
onReconnect?.();
|
|
1808
1816
|
} catch (error) {
|
|
1809
|
-
console.warn(
|
|
1817
|
+
console.warn('[dsh-im] mux reconnect hook failed:', this.#logPrefix, error.message);
|
|
1810
1818
|
}
|
|
1811
1819
|
},
|
|
1812
1820
|
onEnvelope: (envelope) => {
|
|
@@ -1822,13 +1830,13 @@ export class HarnessClient {
|
|
|
1822
1830
|
|| typeof payload.event !== 'object') return;
|
|
1823
1831
|
onSessionEvent({ sessionId: payload.sessionId, event: payload.event });
|
|
1824
1832
|
} catch (error) {
|
|
1825
|
-
console.warn(
|
|
1833
|
+
console.warn('[dsh-im] ignored a malformed global mux frame:', this.#logPrefix, error.message);
|
|
1826
1834
|
}
|
|
1827
1835
|
},
|
|
1828
1836
|
});
|
|
1829
1837
|
} catch (error) {
|
|
1830
1838
|
if (signal.aborted) return;
|
|
1831
|
-
console.warn(
|
|
1839
|
+
console.warn('[dsh-im] Harness event mux disconnected:', this.#logPrefix, error.message);
|
|
1832
1840
|
}
|
|
1833
1841
|
if (signal.aborted) return;
|
|
1834
1842
|
try {
|
|
@@ -1848,7 +1856,7 @@ export class HarnessClient {
|
|
|
1848
1856
|
rpcId: `${this.#rpcIdPrefix}-${randomUUID()}`,
|
|
1849
1857
|
...options,
|
|
1850
1858
|
onMalformed: (error) => {
|
|
1851
|
-
console.warn(
|
|
1859
|
+
console.warn('[dsh-im] ignored a malformed Harness mux frame:', this.#logPrefix, error.message);
|
|
1852
1860
|
},
|
|
1853
1861
|
});
|
|
1854
1862
|
}
|
|
@@ -16,7 +16,13 @@ export function validHarnessQuestion(question) {
|
|
|
16
16
|
))));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** Render the question prompt. `hasButtons` only swaps the closing hint: the
|
|
20
|
+
* numbered list stays, because a channel whose keyboard fails still answers by text.
|
|
21
|
+
*/
|
|
22
|
+
export function harnessQuestionText(question, index, total, {
|
|
23
|
+
requiresMention = false,
|
|
24
|
+
hasButtons = false,
|
|
25
|
+
} = {}) {
|
|
20
26
|
const lines = [];
|
|
21
27
|
const progress = total > 1 ? `(${index + 1}/${total})` : '';
|
|
22
28
|
lines.push(t('DeepSeek Harness 需要你补充信息{progress}:', { progress }));
|
|
@@ -34,7 +40,9 @@ export function harnessQuestionText(question, index, total, { requiresMention =
|
|
|
34
40
|
});
|
|
35
41
|
lines.push('', question.multiSelect === true
|
|
36
42
|
? t('请回复选项序号或文字;多选用逗号分隔,也可补充其他内容。')
|
|
37
|
-
:
|
|
43
|
+
: hasButtons
|
|
44
|
+
? t('请点击下方按钮选择,也可直接回复文字。')
|
|
45
|
+
: t('请回复一个选项序号或文字,也可直接输入其他答案。'));
|
|
38
46
|
} else {
|
|
39
47
|
lines.push('', t('请直接回复你的答案。'));
|
|
40
48
|
}
|
|
@@ -16,6 +16,7 @@ export default {
|
|
|
16
16
|
|
|
17
17
|
'钉钉未能换取图片下载地址,请重新发送;若持续失败,请检查机器人的“企业内机器人发送消息权限”。': 'DingTalk could not provide the image download address. Please resend; if it keeps failing, check the bot\'s "Send messages as an internal robot" permission.',
|
|
18
18
|
'钉钉没有返回图片下载地址,请重新发送。': 'DingTalk did not return an image download address. Please resend.',
|
|
19
|
+
'钉钉未提供引用附件的下载信息,无法读取原附件。请直接重新发送附件后再提问。': 'DingTalk did not provide download information for the quoted attachment, so it cannot be read. Please resend the attachment directly with your question.',
|
|
19
20
|
'钉钉返回的图片临时地址无法读取,请重新发送。': 'The temporary image address returned by DingTalk could not be read. Please resend.',
|
|
20
21
|
'结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。': 'Delivery of the result file "{name}" could not be confirmed. Please check whether it already arrived in the chat before retrying.',
|
|
21
22
|
'结果文件「{name}」已生成,但钉钉应用或机器人缺少文件消息权限。请开通应用 qyapi_base 权限,并确认机器人具备文件消息发送能力。': 'Result file "{name}" was generated, but the DingTalk app or bot lacks file message permission. Enable the app\'s qyapi_base permission and make sure the bot can send file messages.',
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// English translations (qq area). Keys are exact Chinese literals passed to t().
|
|
2
2
|
export default {
|
|
3
|
+
'无法读取 QQ 本地状态,请检查数据目录及访问权限。': 'Unable to read local QQ state. Check the data directory and access permissions.',
|
|
4
|
+
'QQ 本地状态已损坏,但无法备份,原文件已保留。': 'Local QQ state is corrupt and could not be backed up. The original file has been preserved.',
|
|
5
|
+
'无法保存 QQ 本地状态,请检查磁盘空间及目录写入权限。': 'Unable to save local QQ state. Check disk space and directory write permissions.',
|
|
3
6
|
'返回主菜单': 'Back to main menu',
|
|
4
7
|
'模式/预设': 'Mode / preset',
|
|
5
8
|
'归档显示': 'Archived sessions',
|
|
@@ -226,4 +226,15 @@ export default {
|
|
|
226
226
|
'Failed to submit the answer. Please resend your answer to the current question.',
|
|
227
227
|
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。':
|
|
228
228
|
'A pending question left over in this Session was detected. It has been safely cancelled, and your latest message is being processed.',
|
|
229
|
+
// Inline-keyboard question cards (text-harness-bridge.mjs).
|
|
230
|
+
'该问题已处理,无需再次选择。':
|
|
231
|
+
'This question has already been handled; no further choice is needed.',
|
|
232
|
+
'只有发起当前任务的用户可以处理这条问题。':
|
|
233
|
+
'Only the user who started this task can answer this question.',
|
|
234
|
+
'正在提交你的选择,请稍候。': 'Submitting your choice, please wait.',
|
|
235
|
+
'这个选项已失效,请使用最新一条问题。':
|
|
236
|
+
'This option has expired. Please use the most recent question message.',
|
|
237
|
+
'多选问题请直接回复文字。':
|
|
238
|
+
'Please answer a multi-select question by replying with text.',
|
|
239
|
+
'已选择:{label}': 'Selected: {label}',
|
|
229
240
|
};
|
|
@@ -83,6 +83,8 @@ export default {
|
|
|
83
83
|
'Reply with option numbers or text; separate multiple choices with commas, or add anything else.',
|
|
84
84
|
'请回复一个选项序号或文字,也可直接输入其他答案。':
|
|
85
85
|
'Reply with an option number or its text, or type your own answer directly.',
|
|
86
|
+
'请点击下方按钮选择,也可直接回复文字。':
|
|
87
|
+
'Tap a button below to choose, or reply with text directly.',
|
|
86
88
|
'请直接回复你的答案。': 'Please reply with your answer directly.',
|
|
87
89
|
'群聊中请 @机器人 后发送答案。':
|
|
88
90
|
'In group chats, please @ the bot before sending your answer.',
|
|
@@ -6,6 +6,7 @@ export default {
|
|
|
6
6
|
'微信已连接 DeepSeek Harness。': 'WeChat is connected to DeepSeek Harness.',
|
|
7
7
|
'结果文件「{name}」已生成,但微信机器人当前没有文件消息发送权限,请检查机器人文件消息能力。': 'The result file "{name}" was generated, but the WeChat bot currently has no permission to send file messages. Please check the bot\'s file messaging capability.',
|
|
8
8
|
'结果文件「{name}」超过当前微信会话可发送的文件大小,未发送。': 'The result file "{name}" exceeds the file size limit of the current WeChat conversation and was not sent.',
|
|
9
|
+
'结果文件「{name}」上传微信时长时间没有进展,已超时,文件尚未发送。请检查网络后重试,或压缩、拆分文件后发送。': 'The upload of "{name}" to WeChat stalled and timed out. The file has not been sent. Check your network and retry, or compress or split the file before sending.',
|
|
9
10
|
'结果文件「{name}」暂时被微信限流,未能发送,请稍后重试。': 'The result file "{name}" was temporarily rate-limited by WeChat and was not sent. Please try again later.',
|
|
10
11
|
'结果文件「{name}」已生成,但微信拒绝了该文件消息。': 'The result file "{name}" was generated, but WeChat rejected the file message.',
|
|
11
12
|
'结果文件「{name}」已生成,但暂时未能通过微信发送,请稍后重试。': 'The result file "{name}" was generated but could not be sent via WeChat right now. Please try again later.',
|
|
@@ -72,6 +73,7 @@ export default {
|
|
|
72
73
|
'微信文件上传失败(HTTP {status})。': 'The WeChat file upload failed (HTTP {status}).',
|
|
73
74
|
'微信文件上传响应缺少下载参数。': 'The WeChat file upload response is missing the download parameter.',
|
|
74
75
|
'微信文件上传失败。': 'The WeChat file upload failed.',
|
|
76
|
+
'微信文件上传长时间没有进展,已超时。': 'The WeChat file upload stalled and timed out.',
|
|
75
77
|
'拒绝访问不受信任的微信服务地址。': 'Refusing to access an untrusted WeChat service URL.',
|
|
76
78
|
'微信服务请求失败(HTTP {status})。': 'The WeChat service request failed (HTTP {status}).',
|
|
77
79
|
'微信服务返回了无法解析的响应。': 'The WeChat service returned an unparseable response.',
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Session-keyed source guidance for the current conversation.
|
|
2
|
+
//
|
|
3
|
+
// The prompt RPC carries no message source, so a channel publishes the guidance
|
|
4
|
+
// it would otherwise repeat inside every user message when it dispatches the
|
|
5
|
+
// prompt. The Host materializes it once per session as dynamic prompt context
|
|
6
|
+
// (`systemPrompt.context`), which appends a durable snapshot only when the
|
|
7
|
+
// rendered text changes. The registry is module-level because one process owns
|
|
8
|
+
// exactly one, the same shape `outboundArtifactRegistry` uses.
|
|
9
|
+
|
|
10
|
+
/** Prompt-context name the Host materializes this guidance under. */
|
|
11
|
+
export const IM_SOURCE_GUIDANCE_CONTEXT = 'dsh-im:source-guidance';
|
|
12
|
+
|
|
13
|
+
/** Contexts are joined in ascending order; this one follows the policy facts. */
|
|
14
|
+
export const IM_SOURCE_GUIDANCE_ORDER = 125;
|
|
15
|
+
|
|
16
|
+
/** Bound for one session's guidance; the settings cap is the same size. */
|
|
17
|
+
const GUIDANCE_MAX_LENGTH = 8_000;
|
|
18
|
+
|
|
19
|
+
/** Bound on retained sessions, so an abandoned conversation cannot grow it. */
|
|
20
|
+
const MAX_SESSIONS = 1_024;
|
|
21
|
+
|
|
22
|
+
class ImSourceGuidanceRegistry {
|
|
23
|
+
#bySession = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Publish the guidance in force for one session. Empty guidance clears it, so
|
|
27
|
+
* a conversation that turns enhancement off stops contributing a snapshot.
|
|
28
|
+
* @param sessionId - the Session the prompt was dispatched to.
|
|
29
|
+
* @param guidance - the scope's guidance text, or empty when none applies.
|
|
30
|
+
*/
|
|
31
|
+
publish(sessionId, guidance) {
|
|
32
|
+
if (typeof sessionId !== 'string' || !sessionId) return;
|
|
33
|
+
const text = typeof guidance === 'string' ? guidance.slice(0, GUIDANCE_MAX_LENGTH) : '';
|
|
34
|
+
// Delete first so the re-inserted key is the most recently used one.
|
|
35
|
+
this.#bySession.delete(sessionId);
|
|
36
|
+
if (!text.trim()) return;
|
|
37
|
+
this.#bySession.set(sessionId, text);
|
|
38
|
+
while (this.#bySession.size > MAX_SESSIONS) {
|
|
39
|
+
this.#bySession.delete(this.#bySession.keys().next().value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param sessionId - a Session id, or anything else.
|
|
45
|
+
* @returns the guidance in force for that session, or undefined.
|
|
46
|
+
*/
|
|
47
|
+
get(sessionId) {
|
|
48
|
+
return typeof sessionId === 'string' ? this.#bySession.get(sessionId) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Drop one session's guidance.
|
|
53
|
+
* @param sessionId - the Session leaving the registry.
|
|
54
|
+
*/
|
|
55
|
+
forget(sessionId) {
|
|
56
|
+
if (typeof sessionId === 'string') this.#bySession.delete(sessionId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @returns how many sessions currently carry guidance. */
|
|
60
|
+
get size() {
|
|
61
|
+
return this.#bySession.size;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const imSourceGuidance = new ImSourceGuidanceRegistry();
|