@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.21.1",
|
|
4
4
|
"description": "把十一种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -64,6 +64,22 @@
|
|
|
64
64
|
{
|
|
65
65
|
"name": "lyzhu86",
|
|
66
66
|
"url": "https://github.com/lyzhu86"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "Librazy",
|
|
70
|
+
"url": "https://github.com/Librazy"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "Johnslee1207",
|
|
74
|
+
"url": "https://github.com/johnslee1207-commits"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "Dong09",
|
|
78
|
+
"url": "https://github.com/Dong09"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "wings1848",
|
|
82
|
+
"url": "https://github.com/wings1848"
|
|
67
83
|
}
|
|
68
84
|
],
|
|
69
85
|
"license": "MIT",
|
|
@@ -109,13 +109,26 @@ export function DiscordLogoGlyph({ size } = {}) {
|
|
|
109
109
|
export function SlackLogoGlyph({ size } = {}) {
|
|
110
110
|
return h('svg', {
|
|
111
111
|
...dimensions(size),
|
|
112
|
-
viewBox: '0 0
|
|
112
|
+
viewBox: '0 0 100 100',
|
|
113
113
|
focusable: 'false',
|
|
114
114
|
'aria-hidden': 'true',
|
|
115
115
|
'data-im-channel-logo': 'slack',
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
},
|
|
117
|
+
h('path', {
|
|
118
|
+
fill: '#36C5F0',
|
|
119
|
+
d: 'M36.5 0A10.5 10.5 0 0 0 36.5 21H47V10.5A10.5 10.5 0 0 0 36.5 0ZM10.5 26a10.5 10.5 0 0 0 0 21h26a10.5 10.5 0 0 0 0-21Z',
|
|
120
|
+
}),
|
|
121
|
+
h('path', {
|
|
122
|
+
fill: '#2EB67D',
|
|
123
|
+
d: 'M100 36.5A10.5 10.5 0 0 0 79 36.5V47h10.5A10.5 10.5 0 0 0 100 36.5ZM74 10.5a10.5 10.5 0 0 0-21 0v26a10.5 10.5 0 0 0 21 0Z',
|
|
124
|
+
}),
|
|
125
|
+
h('path', {
|
|
126
|
+
fill: '#ECB22E',
|
|
127
|
+
d: 'M63.5 100a10.5 10.5 0 0 0 0-21H53v10.5A10.5 10.5 0 0 0 63.5 100ZM89.5 74a10.5 10.5 0 0 0 0-21h-26a10.5 10.5 0 0 0 0 21Z',
|
|
128
|
+
}),
|
|
129
|
+
h('path', {
|
|
130
|
+
fill: '#E01E5A',
|
|
131
|
+
d: 'M0 63.5a10.5 10.5 0 0 0 21 0V53H10.5A10.5 10.5 0 0 0 0 63.5ZM26 89.5a10.5 10.5 0 0 0 21 0v-26a10.5 10.5 0 0 0-21 0Z',
|
|
119
132
|
}));
|
|
120
133
|
}
|
|
121
134
|
|
|
@@ -138,7 +151,7 @@ export function IMessageLogoGlyph({ size } = {}) {
|
|
|
138
151
|
'aria-hidden': 'true', 'data-im-channel-logo': 'imessage',
|
|
139
152
|
}, h('path', {
|
|
140
153
|
fill: 'currentColor',
|
|
141
|
-
d: 'M12 2.
|
|
154
|
+
d: 'M12 2.5C5.65 2.5.5 6.66.5 11.8c0 2.76 1.49 5.24 3.85 6.94.28 1.34-.35 2.76-1.6 4.01 2.1-.2 3.87-.86 5.26-1.85 1.25.4 2.6.62 3.99.62 6.35 0 11.5-4.35 11.5-9.72S18.35 2.5 12 2.5Z',
|
|
142
155
|
}));
|
|
143
156
|
}
|
|
144
157
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const IMESSAGE_STYLE_ID = 'xmanrui-dsh-im-imessage-settings';
|
|
2
2
|
const CSS = String.raw`
|
|
3
3
|
.dim-pageIMessage { --ddt-accent: #32a852; --ddt-accent-deep: #248a40; --ddt-accent-wash: #edf9f0; }
|
|
4
|
-
.dim-avatarIMessage { color: #fff; background: #
|
|
4
|
+
.dim-avatarIMessage { color: #fff; background: linear-gradient(180deg, #5bf675 0%, #28d944 50%, #0fbd2c 100%); box-shadow: inset 0 1px 1px rgb(255 255 255 / 45%), 0 1px 3px rgb(31 35 41 / 12%); }
|
|
5
5
|
.dim-avatarIMessage svg { display: block; }
|
|
6
6
|
.dim-imessagePermissionPanel { padding: 20px; }
|
|
7
7
|
.dim-imessagePermissionPanel p { color: var(--dsw-alias-label-secondary, #646a73); }
|
|
@@ -2,7 +2,7 @@ export const SLACK_STYLE_ID = 'xmanrui-dsh-im-slack-settings';
|
|
|
2
2
|
|
|
3
3
|
const CSS = String.raw`
|
|
4
4
|
.dsl-page { --ddt-accent: #4a154b; --ddt-accent-deep: #321033; --ddt-accent-wash: #f7eef7; }
|
|
5
|
-
.dsl-avatar {
|
|
5
|
+
.dsl-avatar { background: linear-gradient(145deg, #fff, #f8fafb); border: 1px solid #e5e6eb; }
|
|
6
6
|
.dsl-avatar svg { display: block; }
|
|
7
7
|
.dsl-setup { display: grid; gap: 18px; }
|
|
8
8
|
.dsl-guide { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 18px; padding: 16px; border: 1px solid color-mix(in srgb, #4a154b 18%, var(--dsw-alias-border-l2, #e5e6eb)); border-radius: 11px; background: color-mix(in srgb, #4a154b 4%, var(--dsw-alias-bg-layer-1, #fff)); }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { h, localizeText } from '../../i18n.js';
|
|
3
3
|
import { normalizeConnectionError } from './api.js';
|
|
4
|
+
import { CONFIG_ISSUE_LABELS } from '../../../../src/channels/weixin/diagnostic-details.mjs';
|
|
4
5
|
|
|
5
6
|
const STAGE_LABELS = {
|
|
6
7
|
'startup.load': '加载微信配置', 'qr.begin': '申请二维码', 'qr.encode': '生成二维码图片', 'qr.poll': '查询扫码状态',
|
|
@@ -27,7 +28,7 @@ export function formatWeixinDiagnostic(value) {
|
|
|
27
28
|
return [
|
|
28
29
|
localizeText(error.message), details.hint ? localizeText(details.hint) : null,
|
|
29
30
|
localizeText('错误码') + ': ' + error.code,
|
|
30
|
-
...['operation', 'stage', 'reason', 'httpStatus', 'providerCode', 'resource', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
|
|
31
|
+
...['operation', 'stage', 'reason', 'httpStatus', 'providerCode', 'resource', 'file', 'field', 'issue', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
|
|
31
32
|
.filter(field => details[field] !== undefined)
|
|
32
33
|
.map(field => `${field}: ${details[field]}`),
|
|
33
34
|
].filter(Boolean).join('\n');
|
|
@@ -48,6 +49,8 @@ export function WeixinConnectionError({ error: value, warning = false }) {
|
|
|
48
49
|
const fields = [
|
|
49
50
|
['错误码', error.code], ['失败阶段', localizeText(STAGE_LABELS[details.stage] ?? details.stage ?? '')],
|
|
50
51
|
['底层原因', details.reason], ['HTTP 状态', details.httpStatus], ['微信返回码', details.providerCode],
|
|
52
|
+
['配置文件', details.file], ['配置字段', details.field],
|
|
53
|
+
['校验原因', details.issue ? localizeText(CONFIG_ISSUE_LABELS[details.issue]) : undefined],
|
|
51
54
|
['参考号', details.referenceId], ['发生时间', details.occurredAt], ['插件版本', details.pluginVersion],
|
|
52
55
|
].filter(([, text]) => text !== undefined && text !== '');
|
|
53
56
|
return h('div', { className: 'dxw-summary dim-cardSummary', 'data-weixin-diagnostic': true, role: warning ? 'status' : undefined },
|
|
@@ -1020,6 +1020,14 @@ function channelName(value) {
|
|
|
1020
1020
|
}
|
|
1021
1021
|
|
|
1022
1022
|
function translateDynamic(text) {
|
|
1023
|
+
const configMessage = /^微信配置格式错误:(config\.json|workspaces\.json)。请查看诊断详情,修复后重启 DSH。$/.exec(text);
|
|
1024
|
+
if (configMessage) return EN['微信配置格式错误:{file}。请查看诊断详情,修复后重启 DSH。'].replace('{file}', configMessage[1]);
|
|
1025
|
+
const configHint = /^(.*?)请检查微信渠道数据目录中的 (config\.json|workspaces\.json),修复后重启 DSH;“重新读取”不会重新加载配置。\s*(字段位置中的序号从 0 开始,按文件中的条目顺序计数,不包含真实账号标识。)?$/.exec(text);
|
|
1026
|
+
if (configHint) return [
|
|
1027
|
+
localizeText(configHint[1].trim()),
|
|
1028
|
+
EN['请检查微信渠道数据目录中的 {file},修复后重启 DSH;“重新读取”不会重新加载配置。'].replace('{file}', configHint[2]),
|
|
1029
|
+
configHint[3] ? localizeText(configHint[3]) : '',
|
|
1030
|
+
].filter(Boolean).join(' ');
|
|
1023
1031
|
const guidanceLimit = /^增强提示词不得超过 (\d+) 个字符。$/.exec(text);
|
|
1024
1032
|
if (guidanceLimit) return `Guidance must not exceed ${guidanceLimit[1]} characters.`;
|
|
1025
1033
|
let match = /^(\d+) \/ (\d+) 在线$/.exec(text);
|
|
@@ -141,6 +141,8 @@ export function ModelEditor({ model = null, disabled = false, onSave }) {
|
|
|
141
141
|
'aria-label': '查看模型设置说明', 'aria-describedby': `${id}-help` },
|
|
142
142
|
h('span', { 'aria-hidden': true }, '?')),
|
|
143
143
|
h('span', { id: `${id}-help`, className: 'dim-presetTooltip', role: 'tooltip' },
|
|
144
|
+
effortHint && !effortUnavailable
|
|
145
|
+
? h('span', { id: `${id}-hint` }, effortHint, ' ') : null,
|
|
144
146
|
'只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。'))),
|
|
145
147
|
saving ? h('span', { className: 'dim-presetStatus', role: 'status' }, '保存中…') : null),
|
|
146
148
|
row('model', '模型', entry?.name ?? (current ? modelSelectionId(current) : localizeText('跟随默认模型'))),
|
|
@@ -163,8 +165,8 @@ export function ModelEditor({ model = null, disabled = false, onSave }) {
|
|
|
163
165
|
...(reasoning?.efforts ?? []).map((level) => option(`effort:${level.id}`, level.name, level.description,
|
|
164
166
|
effort === level.id, { ...current, reasoningEffort: level.id })),
|
|
165
167
|
]) : null,
|
|
166
|
-
effortHint ? h('p', { id: `${id}-hint`, className: 'dim-modelHint',
|
|
167
|
-
role:
|
|
168
|
+
effortHint && effortUnavailable ? h('p', { id: `${id}-hint`, className: 'dim-modelHint',
|
|
169
|
+
role: 'status' }, effortHint) : null,
|
|
168
170
|
error || !currentAvailable ? h('p', { className: 'dim-presetError', role: error ? 'alert' : 'status' },
|
|
169
171
|
error ?? '当前模型已不可用,请选择其他模型或跟随默认模型。') : null);
|
|
170
172
|
}
|
|
@@ -26,8 +26,7 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
|
26
26
|
const installations = new WeakMap();
|
|
27
27
|
|
|
28
28
|
function hasClassPart(element, part) {
|
|
29
|
-
|
|
30
|
-
return [...element.classList].some((token) => match.test(token));
|
|
29
|
+
return [...element.classList].some((token) => token.split(/[_-]/u).includes(part));
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
function titleOf(row) {
|
|
@@ -126,10 +126,11 @@ const CSS = String.raw`
|
|
|
126
126
|
.dim-logoOffice svg { width: 23px; height: 23px; }
|
|
127
127
|
.dim-logoDiscord { color: white; background: #5865f2; }
|
|
128
128
|
.dim-logoDiscord svg { width: 21px; height: 21px; }
|
|
129
|
-
.dim-logoSlack {
|
|
129
|
+
.dim-logoSlack { background: linear-gradient(145deg, #fff, #f8fafb); border: 1px solid #e5e6eb; }
|
|
130
130
|
.dim-logoSlack svg { width: 21px; height: 21px; }
|
|
131
131
|
.dim-logoWhatsapp { color: white; background: #25d366; }
|
|
132
|
-
.dim-logoIMessage { color: white; background: #
|
|
132
|
+
.dim-logoIMessage { color: white; background: linear-gradient(180deg, #5bf675 0%, #28d944 50%, #0fbd2c 100%); box-shadow: inset 0 1px 1px rgb(255 255 255 / 45%), 0 1px 3px rgb(31 35 41 / 12%); }
|
|
133
|
+
.dim-logoIMessage svg { width: 23px; height: 23px; }
|
|
133
134
|
.dim-logoWhatsapp svg { width: 21px; height: 21px; }
|
|
134
135
|
.dim-channelCopy { min-width: 0; display: grid; }
|
|
135
136
|
.dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -74,7 +74,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
74
74
|
const stateFor = async (botId) => {
|
|
75
75
|
let state = stateStores.get(botId);
|
|
76
76
|
if (!state) {
|
|
77
|
-
state = await new StateStore(statePath(botId)).load();
|
|
77
|
+
state = await new StateStore(statePath(botId), { logger }).load();
|
|
78
78
|
stateStores.set(botId, state);
|
|
79
79
|
}
|
|
80
80
|
return state;
|
|
@@ -9,6 +9,7 @@ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from
|
|
|
9
9
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
|
10
10
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
11
11
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
12
|
+
import { publicQqStateError } from '../../../../src/channels/qq/state-error.mjs';
|
|
12
13
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
|
13
14
|
import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
|
|
14
15
|
|
|
@@ -232,7 +233,7 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
|
|
232
233
|
const workspaceError = publicWorkspaceError(error);
|
|
233
234
|
return signal?.aborted
|
|
234
235
|
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
235
|
-
: { ok: false, error: workspaceError
|
|
236
|
+
: { ok: false, error: publicQqStateError(error) ?? workspaceError
|
|
236
237
|
?? { code: 'qq-operation-failed', message: 'QQ 操作失败,请稍后重试。' } };
|
|
237
238
|
}
|
|
238
239
|
};
|
|
@@ -17,6 +17,7 @@ import { installDeliveryRpc } from './delivery-rpc.mjs';
|
|
|
17
17
|
import { installDeliveryHttp } from './delivery-http.mjs';
|
|
18
18
|
import { createDeliveryService } from './delivery-service.mjs';
|
|
19
19
|
import { installInboundTtlRpc } from './inbound-ttl-rpc.mjs';
|
|
20
|
+
import { installInjectedContext } from './injected-context.mjs';
|
|
20
21
|
import { installSessionSyncCoordinator } from './session-sync-coordinator.mjs';
|
|
21
22
|
import { installSessionTitlePrefix } from './session-title-prefix.mjs';
|
|
22
23
|
import { installUpdateRpc } from './update-rpc.mjs';
|
|
@@ -41,6 +42,7 @@ export function createImHostPlugin(internals = {}) {
|
|
|
41
42
|
const startHostLanguageRpc = internals.installHostLanguageRpc ?? installHostLanguageRpc;
|
|
42
43
|
const startUpdate = internals.installUpdateRpc ?? installUpdateRpc;
|
|
43
44
|
const startInboundTtl = internals.installInboundTtlRpc ?? installInboundTtlRpc;
|
|
45
|
+
const startInjectedContext = internals.installInjectedContext ?? installInjectedContext;
|
|
44
46
|
const startDelivery = internals.installDeliveryRpc ?? installDeliveryRpc;
|
|
45
47
|
const startDeliveryHttp = internals.installDeliveryHttp ?? installDeliveryHttp;
|
|
46
48
|
const startSessionSync = internals.installSessionSyncCoordinator
|
|
@@ -139,6 +141,11 @@ export function createImHostPlugin(internals = {}) {
|
|
|
139
141
|
const logger = typeof ctx?.logger === 'function'
|
|
140
142
|
? ctx.logger(name)
|
|
141
143
|
: (ctx?.logger ?? console);
|
|
144
|
+
try {
|
|
145
|
+
startInjectedContext(ctx, { logger });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.error?.('[dsh-im] failed to activate injected-context pairing; prompts keep the inline prefix', error);
|
|
148
|
+
}
|
|
142
149
|
if (ctx?.connection?.fetch) {
|
|
143
150
|
if (hostLanguage) {
|
|
144
151
|
try {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { getImHostLanguage } from '../../src/channels/shared/i18n.mjs';
|
|
2
|
+
import { rewriteInjectedContextMessages } from '../../src/channels/shared/injected-context.mjs';
|
|
3
|
+
import {
|
|
4
|
+
IM_SOURCE_GUIDANCE_CONTEXT,
|
|
5
|
+
IM_SOURCE_GUIDANCE_ORDER,
|
|
6
|
+
imSourceGuidance,
|
|
7
|
+
} from '../../src/channels/shared/im-source-guidance.mjs';
|
|
8
|
+
|
|
9
|
+
/** Row label for a quoted reply, in the mirrored Host interface language. */
|
|
10
|
+
const REPLY_LABELS = Object.freeze({ zh: '\u5f15\u7528', en: 'Quoted' });
|
|
11
|
+
|
|
12
|
+
/** Row label for a source block that names no readable field of its own. */
|
|
13
|
+
const SOURCE_LABELS = Object.freeze({ zh: '\u6765\u6e90', en: 'Source' });
|
|
14
|
+
|
|
15
|
+
/** Read the current reply-row label; an unknown language falls back to English. */
|
|
16
|
+
function replyLabel() {
|
|
17
|
+
try {
|
|
18
|
+
return getImHostLanguage() === 'en' ? REPLY_LABELS.en : REPLY_LABELS.zh;
|
|
19
|
+
} catch {
|
|
20
|
+
return REPLY_LABELS.en;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Read the current nameless-source-row label; unknown languages fall back. */
|
|
25
|
+
function sourceLabel() {
|
|
26
|
+
try {
|
|
27
|
+
return getImHostLanguage() === 'en' ? SOURCE_LABELS.en : SOURCE_LABELS.zh;
|
|
28
|
+
} catch {
|
|
29
|
+
return SOURCE_LABELS.en;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pair every injected context block with the user message that carried it, and
|
|
35
|
+
* materialize the source guidance as session-level prompt context.
|
|
36
|
+
*
|
|
37
|
+
* The prompt RPC cannot carry a message source, so a channel writes its source
|
|
38
|
+
* block, guidance and quoted reply into the prompt text. `agent/pre-step` is
|
|
39
|
+
* the one point where the claimed messages are already known and nothing is
|
|
40
|
+
* committed yet: splitting the blocks there records each as its own
|
|
41
|
+
* plugin-sourced message next to exactly the user message it belongs to,
|
|
42
|
+
* whatever the inbox interleaved meanwhile. A Host without the Agent loop never
|
|
43
|
+
* dispatches the event, and the prompt then keeps its inline blocks.
|
|
44
|
+
*
|
|
45
|
+
* Guidance is not per message but per conversation, so it is registered as
|
|
46
|
+
* dynamic prompt context instead: the Host appends one durable snapshot per
|
|
47
|
+
* Session and only re-renders it when a channel publishes different guidance.
|
|
48
|
+
* Deployment policy still decides whether such snapshots exist at all, which is
|
|
49
|
+
* why the message-side copy is kept until a channel has published a matching
|
|
50
|
+
* value for that Session.
|
|
51
|
+
*
|
|
52
|
+
* @param ctx - owning Host context.
|
|
53
|
+
* @param options.logger - Host logger for a rewrite that could not be trusted.
|
|
54
|
+
* @param options.registry - guidance registry; tests inject their own.
|
|
55
|
+
* @returns the Cordis disposer, or null when the context cannot listen.
|
|
56
|
+
*/
|
|
57
|
+
export function installInjectedContext(ctx, { logger, registry = imSourceGuidance } = {}) {
|
|
58
|
+
if (typeof ctx?.on !== 'function') return null;
|
|
59
|
+
|
|
60
|
+
const startGuidanceContext = (promptCtx) => {
|
|
61
|
+
if (typeof promptCtx?.systemPrompt?.context !== 'function') return null;
|
|
62
|
+
return promptCtx.systemPrompt.context({
|
|
63
|
+
name: IM_SOURCE_GUIDANCE_CONTEXT,
|
|
64
|
+
order: IM_SOURCE_GUIDANCE_ORDER,
|
|
65
|
+
text: (assembly) => registry.get(assembly?.agent?.session?.id) ?? '',
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
if (typeof ctx.inject === 'function') {
|
|
69
|
+
// Same gating the outbound artifact tool uses: register once the service
|
|
70
|
+
// exists instead of requiring it at load time.
|
|
71
|
+
ctx.inject(['systemPrompt'], startGuidanceContext);
|
|
72
|
+
} else {
|
|
73
|
+
startGuidanceContext(ctx);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const disposeDisposed = ctx.on('agent/disposed', ({ agent }) => {
|
|
77
|
+
const sessionId = agent?.session?.id;
|
|
78
|
+
if (typeof sessionId === 'string') registry.forget(sessionId);
|
|
79
|
+
}, { global: true });
|
|
80
|
+
|
|
81
|
+
const listener = async ({ agent }, next) => {
|
|
82
|
+
const decision = await next();
|
|
83
|
+
if (decision === null || typeof decision !== 'object' || decision.kind !== 'enter') {
|
|
84
|
+
return decision;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const rewritten = rewriteInjectedContextMessages(decision.messages, {
|
|
88
|
+
labels: { reply: replyLabel(), source: sourceLabel() },
|
|
89
|
+
ownedGuidance: registry.get(agent?.session?.id),
|
|
90
|
+
});
|
|
91
|
+
return rewritten === null ? decision : { ...decision, messages: rewritten };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// A failed split must never cost the user their message: the blocks then
|
|
94
|
+
// stay inline, which is the same fallback a Host without Agents uses.
|
|
95
|
+
logger?.warn?.('[dsh-im] unable to split the injected context blocks:', error?.message ?? error);
|
|
96
|
+
return decision;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const disposePreStep = ctx.on('agent/pre-step', listener, { global: true });
|
|
100
|
+
return () => {
|
|
101
|
+
disposePreStep?.();
|
|
102
|
+
disposeDisposed?.();
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -474,13 +474,97 @@ class ModernHarnessApi {
|
|
|
474
474
|
#requestQuestion(request, next) {
|
|
475
475
|
const owner = this.#claimableAgent(request?.agent);
|
|
476
476
|
if (!owner) return next();
|
|
477
|
-
|
|
477
|
+
const turnSignal = request.signal;
|
|
478
|
+
if (turnSignal?.aborted) {
|
|
478
479
|
return Promise.reject(questionError(
|
|
479
480
|
'ask_user_question was aborted before the user answered', 'ASK_ABORTED',
|
|
480
481
|
));
|
|
481
482
|
}
|
|
482
|
-
|
|
483
|
-
|
|
483
|
+
// Give this question a lifetime of its own instead of lending it the enclosing
|
|
484
|
+
// turn's. `dsh-agent-loop` mints one abort controller per turn and every tool
|
|
485
|
+
// call in that turn shares the one signal, and under PTC two consumers of it
|
|
486
|
+
// react to the abort EVENT rather than to `signal.aborted`: `run_code` flips its
|
|
487
|
+
// own program controller from an abort listener (dsh-tools `onOuterAbort`) and
|
|
488
|
+
// the worker code runtime retires the worker the same way
|
|
489
|
+
// (dsh-code-runtime-worker-thread `onAbort`). Retiring the Web card by firing
|
|
490
|
+
// that event therefore also killed the program that was merely waiting for this
|
|
491
|
+
// answer — `code run failed (abort)`. A private controller keeps the retirement
|
|
492
|
+
// below local: real upstream cancellation is forwarded into it, and every
|
|
493
|
+
// listener on this waterfall — the Remote/Web forwarder that renders the card,
|
|
494
|
+
// and our own IM pending — sees a lifetime that ends with this question rather
|
|
495
|
+
// than with the turn.
|
|
496
|
+
//
|
|
497
|
+
// Handing it downstream means replacing `request.signal`, because the Cordis
|
|
498
|
+
// waterfall passes the same request object to every remaining listener and
|
|
499
|
+
// `next()` takes no replacement arguments. The object is a per-call copy made by
|
|
500
|
+
// `UserQuestionService.ask`, and the signal it carried stays reachable here as
|
|
501
|
+
// `turnSignal`, so the caller's own view of the request is untouched.
|
|
502
|
+
const question = new AbortController();
|
|
503
|
+
const forwardTurnAbort = () => question.abort(turnSignal?.reason);
|
|
504
|
+
turnSignal?.addEventListener('abort', forwardTurnAbort, { once: true });
|
|
505
|
+
request.signal = question.signal;
|
|
506
|
+
// Race the IM answer against the host's own answerers (Web/CLI). Claiming the
|
|
507
|
+
// request exclusively used to keep the question out of Web entirely, leaving
|
|
508
|
+
// whoever happened to be looking at that Session with a card they could not
|
|
509
|
+
// answer. Racing keeps both surfaces usable: the first answer wins.
|
|
510
|
+
const im = this.#askThroughIm(request, owner);
|
|
511
|
+
let answeredByIm = false;
|
|
512
|
+
const native = Promise.resolve()
|
|
513
|
+
.then(() => next())
|
|
514
|
+
.catch((error) => {
|
|
515
|
+
// A pure-IM session has no client connected, so the host rejects with
|
|
516
|
+
// NO_PROVIDER — an expected state rather than a failure — and ASK_ABORTED
|
|
517
|
+
// is this adapter's own retire signal below. Any other error is real, so
|
|
518
|
+
// log it instead of swallowing it silently; the question still stays open
|
|
519
|
+
// for IM rather than being failed outright.
|
|
520
|
+
if (error?.code !== 'NO_PROVIDER' && error?.code !== 'ASK_ABORTED') {
|
|
521
|
+
console.warn(
|
|
522
|
+
'[dsh-im] the host user-question answerer failed; waiting for the IM answer:',
|
|
523
|
+
error?.message ?? error,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return new Promise(() => {});
|
|
527
|
+
});
|
|
528
|
+
return Promise.race([
|
|
529
|
+
native,
|
|
530
|
+
im.promise.then((value) => {
|
|
531
|
+
answeredByIm = true;
|
|
532
|
+
return value;
|
|
533
|
+
}),
|
|
534
|
+
]).finally(() => {
|
|
535
|
+
// Losing side cleanup. A host answer already settled this request, so drop
|
|
536
|
+
// the IM pending: leaving it registered would let a late press answer a
|
|
537
|
+
// question the model has moved past. Settling is idempotent.
|
|
538
|
+
im.dismiss(new Error('another client answered this question'));
|
|
539
|
+
turnSignal?.removeEventListener('abort', forwardTurnAbort);
|
|
540
|
+
if (answeredByIm) {
|
|
541
|
+
// The host's own answerer (DSH Web) keeps its question card until its own
|
|
542
|
+
// pending settles, and a host-side adapter has no other handle on it. A Web
|
|
543
|
+
// client holds that pending through the lifetime this question handed
|
|
544
|
+
// downstream, so ending that lifetime makes the client drop a card whose
|
|
545
|
+
// answer was already given on IM — otherwise it keeps offering choices for a
|
|
546
|
+
// question the model has moved past.
|
|
547
|
+
//
|
|
548
|
+
// Ending it is `abort`, not a dispatched `abort` event: a listener that
|
|
549
|
+
// reads `signal.aborted` must see the same thing one that listens for the
|
|
550
|
+
// event does. Either way it reaches only this question's controller, so the
|
|
551
|
+
// turn and every other tool call in it — a waiting `run_code` program above
|
|
552
|
+
// all — keep running. The reason carries `ASK_ABORTED` so the host answerer
|
|
553
|
+
// rejecting alongside us stays an expected retirement rather than a logged
|
|
554
|
+
// failure.
|
|
555
|
+
question.abort(questionError('the question was answered on IM', 'ASK_ABORTED'));
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Present one question to IM clients. Returns the answer promise plus a
|
|
562
|
+
* `dismiss` used to retire the pending when the host answers first.
|
|
563
|
+
*/
|
|
564
|
+
#askThroughIm(request, owner) {
|
|
565
|
+
let pending = null;
|
|
566
|
+
const promise = new Promise((resolve, reject) => {
|
|
567
|
+
pending = {
|
|
484
568
|
rpcId: randomUUID(),
|
|
485
569
|
sessionId: owner.sessionId,
|
|
486
570
|
questions: request.questions,
|
|
@@ -505,6 +589,10 @@ class ModernHarnessApi {
|
|
|
505
589
|
request.signal?.addEventListener('abort', onAbort, { once: true });
|
|
506
590
|
this.#broadcast(this.#questionFrame(pending).payload, pending.rpcId);
|
|
507
591
|
});
|
|
592
|
+
return {
|
|
593
|
+
promise,
|
|
594
|
+
dismiss: (reason) => pending?.settle('cancelled', reason),
|
|
595
|
+
};
|
|
508
596
|
}
|
|
509
597
|
|
|
510
598
|
#approvalFrame(pending) {
|
|
@@ -22,12 +22,15 @@ const built = await build({
|
|
|
22
22
|
define: { 'process.env.NODE_ENV': '"development"' },
|
|
23
23
|
});
|
|
24
24
|
const htmlPath = join(output, 'preview.html');
|
|
25
|
+
const scriptOpen = '<scr' + 'ipt>';
|
|
26
|
+
const scriptClose = '</scr' + 'ipt>';
|
|
27
|
+
const scriptBody = built.outputFiles[0].text.replace(/<\/script/giu, '<\\/script');
|
|
25
28
|
await writeFile(htmlPath, `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>DSH-IM Model Settings</title>
|
|
26
29
|
<style>
|
|
27
30
|
body{margin:0;padding:24px;background:#fff;color:#1f2329;font:14px/20px -apple-system,BlinkMacSystemFont,sans-serif}#app{max-width:760px;margin:auto}#result{max-width:760px;margin:18px auto;font:11px/17px monospace;white-space:pre-wrap;color:#687380}
|
|
28
31
|
@media(max-width:600px){body{padding:12px}}
|
|
29
32
|
@media(prefers-color-scheme:dark){:root{color-scheme:dark;--dsw-alias-bg-layer-1:#202125;--dsw-alias-bg-layer-3:#282a30;--dsw-specific-menu:#282a30;--dsw-alias-bg-module-platform:#25272c;--dsw-alias-label-primary:#e9ebef;--dsw-alias-label-secondary:#b1b5bf;--dsw-alias-label-tertiary:#9a9faa;--dsw-alias-border-l1:#34363c;--dsw-alias-border-l2:#41434c;--dsw-alias-border-l3:#7b9ff9;--dsw-alias-interactive-bg-hover:#353842}body{background:#202125;color:#e9ebef}}
|
|
30
|
-
</style><body><div id="app"></div><pre id="result">Running browser checks…</pre
|
|
33
|
+
</style><body><div id="app"></div><pre id="result">Running browser checks…</pre>${scriptOpen}${scriptBody}${scriptClose}</body></html>`);
|
|
31
34
|
for (const [name, size, extra, query] of [
|
|
32
35
|
['desktop', '1100,1100', [], ''],
|
|
33
36
|
['mobile', '390,1250', [], '?mobile'],
|
|
@@ -238,7 +238,9 @@ if (/(?:from\s*|import\s*\(|require\s*\()\s*["'](?:@larksuiteoapi\/node-sdk|@whi
|
|
|
238
238
|
if (!/(?:from\s*|import\s*\()\s*["']undici["']/.test(host)) {
|
|
239
239
|
throw new Error('host bundle must retain undici as an external runtime dependency');
|
|
240
240
|
}
|
|
241
|
-
if ((executable.mode & 0o111) === 0)
|
|
241
|
+
if (process.platform !== 'win32' && (executable.mode & 0o111) === 0) {
|
|
242
|
+
throw new Error('dsh-im CLI is not executable');
|
|
243
|
+
}
|
|
242
244
|
if (/private-bot-token|must-be-rolled-back|DEEPSEEK_API_KEY=/.test(client + host)) {
|
|
243
245
|
throw new Error('built artifacts contain a test or environment secret marker');
|
|
244
246
|
}
|