@xmanrui/dsh-im 4.25.0 → 4.26.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/PROACTIVE_DELIVERY.en.md +12 -4
- package/PROACTIVE_DELIVERY.md +12 -4
- package/README.en.md +5 -1
- package/README.md +5 -1
- package/lib/client.js +91 -33
- package/lib/index.js +100 -100
- package/package.json +4 -7
- package/plugin-src/client/global-settings.js +26 -14
- package/plugin-src/client/i18n.js +4 -0
- package/plugin-src/client/styles.js +14 -0
- package/plugin-src/client/update-panel.js +6 -0
- package/plugin-src/host/delivery-rpc.mjs +7 -2
- package/plugin-src/host/delivery-service.mjs +8 -2
- package/scripts/verify-injected-context.mjs +120 -0
- package/scripts/verify-package.mjs +1 -0
- package/src/channels/feishu/feishu-runtime.mjs +13 -3
- package/src/channels/shared/i18n-en/image-input.mjs +1 -0
- package/src/channels/shared/injected-context.mjs +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.26.0",
|
|
4
4
|
"description": "把十二种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect twelve IM channels and a public AI Office to a local DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"lib",
|
|
117
117
|
"plugin-src",
|
|
118
118
|
"scripts/verify-interface-language.mjs",
|
|
119
|
+
"scripts/verify-injected-context.mjs",
|
|
119
120
|
"scripts/verify-lan-management.mjs",
|
|
120
121
|
"scripts/verify-model-setting.mjs",
|
|
121
122
|
"scripts/verify-package.mjs",
|
|
@@ -145,13 +146,9 @@
|
|
|
145
146
|
"platform": "web"
|
|
146
147
|
},
|
|
147
148
|
"compatibility": {
|
|
148
|
-
"dsh": "0.1.
|
|
149
|
+
"dsh": "0.1.7-alpha.1",
|
|
149
150
|
"dshReleases": {
|
|
150
|
-
"0.1.
|
|
151
|
-
"0.1.2-alpha.5": "compatible",
|
|
152
|
-
"0.1.2-rc.1": "compatible",
|
|
153
|
-
"0.1.3-alpha.1": "compatible",
|
|
154
|
-
"0.1.5-alpha.1": "compatible"
|
|
151
|
+
"0.1.7-alpha.1": "compatible"
|
|
155
152
|
},
|
|
156
153
|
"profiles": [
|
|
157
154
|
"web"
|
|
@@ -118,21 +118,33 @@ export function ImageInputSettings({ rpcCall }) {
|
|
|
118
118
|
['maxTotalMb', '模型图片总量上限 (MB)'],
|
|
119
119
|
['maxImages', '每条消息最多图片数'],
|
|
120
120
|
];
|
|
121
|
-
return h('section', { className: 'dim-globalSection', 'aria-label': '图片输入', 'aria-busy': phase === 'loading' || phase === 'saving' },
|
|
122
|
-
h('
|
|
123
|
-
|
|
121
|
+
return h('section', { className: 'dim-globalSection dim-imageSettings', 'aria-label': '图片输入', 'aria-busy': phase === 'loading' || phase === 'saving' },
|
|
122
|
+
h('div', { className: 'dim-globalHead' },
|
|
123
|
+
h('div', { className: 'dim-globalHeadTitle' },
|
|
124
|
+
h('h3', null, '图片输入'),
|
|
125
|
+
h('div', { className: 'dim-globalTtlHelp' },
|
|
126
|
+
h('button', {
|
|
127
|
+
type: 'button',
|
|
128
|
+
className: 'dim-channelHelpButton dim-globalTtlHelpButton',
|
|
129
|
+
'aria-label': '查看图片输入说明',
|
|
130
|
+
'aria-describedby': `${id}-help`,
|
|
131
|
+
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
|
132
|
+
h('div', { id: `${id}-help`, className: 'dim-globalTtlTooltip dim-imageSettingsTooltip', role: 'tooltip' },
|
|
133
|
+
'适用于支持图片的聊天渠道。原图按附件保留时长保存,发送给模型的副本会自动缩放或压缩;无法直接发送时交给模型按文件处理。')))),
|
|
124
134
|
h('form', { onSubmit: save },
|
|
125
|
-
|
|
126
|
-
h('
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
h('div', { className: 'dim-imageSettingsFields' },
|
|
136
|
+
...fields.map(([key, label]) => h('div', { className: 'dim-imageSettingsField', key },
|
|
137
|
+
h('label', { htmlFor: `${id}-${key}` }, label),
|
|
138
|
+
h('input', { id: `${id}-${key}`, className: 'dim-globalTtlInput', type: 'number', min: 1, step: 1,
|
|
139
|
+
value: values[key], disabled: phase !== 'ready',
|
|
140
|
+
onChange: (event) => { setValues((current) => ({ ...current, [key]: event.target.value })); setSaved(false); setError(null); },
|
|
141
|
+
})))),
|
|
142
|
+
h('div', { className: 'dim-imageSettingsActions' },
|
|
143
|
+
h(GlobalButton, { type: 'submit', kind: 'primary', className: 'dim-globalSaveButton', disabled: phase !== 'ready' }, phase === 'saving' ? '保存中…' : '保存图片设置'),
|
|
144
|
+
phase === 'error' ? h(GlobalButton, { className: 'dim-globalSaveButton', onClick: () => void load() }, '重试') : null)),
|
|
145
|
+
error ? h('p', { className: 'dim-globalInline dim-imageSettingsFeedback', 'data-tone': 'error', role: 'alert' }, error) : null,
|
|
146
|
+
saved ? h('p', { className: 'dim-globalInline dim-imageSettingsFeedback', role: 'status' }, '已保存') : null,
|
|
147
|
+
phase === 'loading' ? h('p', { className: 'dim-globalInline dim-imageSettingsFeedback', role: 'status' }, '正在读取图片设置…') : null);
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
export function GlobalSettingsPanel({ rpcCall }) {
|
|
@@ -108,6 +108,10 @@ const EN = Object.freeze({
|
|
|
108
108
|
'当前版本': 'Current version',
|
|
109
109
|
'DSH-IM 更新': 'DSH-IM update',
|
|
110
110
|
'检查更新': 'Check for updates',
|
|
111
|
+
'DSH 版本兼容说明': 'DSH version compatibility',
|
|
112
|
+
'dsh-im 4.25.0 支持到 DSH v0.1.6-alpha.2。': 'dsh-im 4.25.0 supports DSH up to v0.1.6-alpha.2.',
|
|
113
|
+
'dsh-im 4.26.0 仅支持 DSH v0.1.7-alpha.1。': 'dsh-im 4.26.0 only supports DSH v0.1.7-alpha.1.',
|
|
114
|
+
'升级到 dsh-im 4.26.0 前,请先将 DSH 升级到 v0.1.7-alpha.1;使用旧版 DSH 请保留 dsh-im 4.25.0。': 'Before updating to dsh-im 4.26.0, upgrade DSH to v0.1.7-alpha.1. If you use an older DSH version, stay on dsh-im 4.25.0.',
|
|
111
115
|
'重新检查': 'Check again',
|
|
112
116
|
'刷新状态': 'Refresh status',
|
|
113
117
|
'手工更新': 'Manual update',
|
|
@@ -613,6 +613,20 @@ const CSS = String.raw`
|
|
|
613
613
|
.dim-generalSettingsTab:focus-visible { outline: 2px solid var(--dim-blue); outline-offset: -2px; border-radius: 5px; }
|
|
614
614
|
.dim-generalSettingsTabPanel { min-width: 0; padding-top: 14px; }
|
|
615
615
|
.dim-globalSection { min-width: 0; padding: 14px 16px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; background: var(--dsw-alias-bg-layer-3, #fff); }
|
|
616
|
+
.dim-globalSection + .dim-globalSection { margin-top: 12px; }
|
|
617
|
+
.dim-imageSettings { container-type: inline-size; }
|
|
618
|
+
.dim-imageSettings .dim-globalHead { position: relative; }
|
|
619
|
+
.dim-imageSettings .dim-globalTtlHelp { position: static; }
|
|
620
|
+
.dim-globalTtlTooltip.dim-imageSettingsTooltip { width: min(320px, 100%); max-width: none; color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 17px; white-space: normal; overflow-wrap: anywhere; }
|
|
621
|
+
.dim-imageSettingsFields { min-width: 0; display: grid; gap: 12px; margin-top: 12px; }
|
|
622
|
+
.dim-imageSettingsField { min-width: 0; display: grid; grid-template-columns: minmax(0, 220px) minmax(0, 160px); align-items: center; gap: 6px 16px; }
|
|
623
|
+
.dim-imageSettingsField label { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
|
|
624
|
+
.dim-imageSettingsField .dim-globalTtlInput { width: 100%; min-width: 0; }
|
|
625
|
+
.dim-imageSettingsActions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
|
|
626
|
+
.dim-globalInline.dim-imageSettingsFeedback { margin: 10px 0 0; }
|
|
627
|
+
@container (max-width: 380px) {
|
|
628
|
+
.dim-imageSettingsField { grid-template-columns: minmax(0, 1fr); }
|
|
629
|
+
}
|
|
616
630
|
.dim-globalHead { min-width: 0; display: flex; align-items: center; }
|
|
617
631
|
.dim-globalHeadTitle { min-width: 0; display: inline-flex; align-items: center; gap: 6px; }
|
|
618
632
|
.dim-globalHead h3 { min-width: 0; overflow: hidden; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; line-height: 22px; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -467,6 +467,12 @@ export function UpdatePanel({ rpcCall, clientVersion, onStatus }) {
|
|
|
467
467
|
versionsDiffer && !restartRequired ? h('p', { className: 'dim-updateHint' }, '页面版本与运行版本不同,请手动刷新页面;若仍不一致,请手动重启 Harness 或 Desktop。') : null,
|
|
468
468
|
canConfirm ? h('p', { className: 'dim-updateHint' },
|
|
469
469
|
'请在机器人空闲时安装;安装会修改当前 profile 的依赖,完成后需手动重启。') : null,
|
|
470
|
+
h('aside', { className: 'dim-updateHint', 'aria-label': 'DSH 版本兼容说明' },
|
|
471
|
+
h('strong', null, 'DSH 版本兼容说明'),
|
|
472
|
+
h('p', { className: 'dim-updateManualHint' }, 'dsh-im 4.25.0 支持到 DSH v0.1.6-alpha.2。'),
|
|
473
|
+
h('p', { className: 'dim-updateManualHint' }, 'dsh-im 4.26.0 仅支持 DSH v0.1.7-alpha.1。'),
|
|
474
|
+
h('p', { className: 'dim-updateManualHint' },
|
|
475
|
+
'升级到 dsh-im 4.26.0 前,请先将 DSH 升级到 v0.1.7-alpha.1;使用旧版 DSH 请保留 dsh-im 4.25.0。')),
|
|
470
476
|
h(ManualUpdateCommand, {
|
|
471
477
|
key: manualCommand ?? 'unavailable', command: manualCommand,
|
|
472
478
|
disabled: busyAction || activeJob || uncertainInstall,
|
|
@@ -65,7 +65,9 @@ function validDraftTarget(value) {
|
|
|
65
65
|
function validPayload(endpoint, payload) {
|
|
66
66
|
if (!ENDPOINTS.has(endpoint) || !isRecord(payload)) return false;
|
|
67
67
|
if (endpoint === DELIVERY_ENDPOINTS.send) {
|
|
68
|
-
return exactKeys(payload, ['botId', 'targetId', 'text'])
|
|
68
|
+
return (exactKeys(payload, ['botId', 'targetId', 'text'])
|
|
69
|
+
|| (exactKeys(payload, ['botId', 'targetId', 'text', 'format'])
|
|
70
|
+
&& ['plain', 'markdown'].includes(payload.format)))
|
|
69
71
|
&& validBotId(payload.botId) && validTargetId(payload.targetId)
|
|
70
72
|
&& typeof payload.text === 'string' && Boolean(payload.text.trim());
|
|
71
73
|
}
|
|
@@ -128,7 +130,10 @@ export function createDeliveryRpcHandler(service) {
|
|
|
128
130
|
try {
|
|
129
131
|
let value;
|
|
130
132
|
if (endpoint === DELIVERY_ENDPOINTS.send) {
|
|
131
|
-
value = await service.send(payload.botId, payload.targetId, payload.text, {
|
|
133
|
+
value = await service.send(payload.botId, payload.targetId, payload.text, {
|
|
134
|
+
signal,
|
|
135
|
+
...(payload.format === undefined ? {} : { format: payload.format }),
|
|
136
|
+
});
|
|
132
137
|
} else if (endpoint === DELIVERY_ENDPOINTS.listTargets) {
|
|
133
138
|
value = await service.listTargets(payload.botId);
|
|
134
139
|
} else if (endpoint === DELIVERY_ENDPOINTS.listSuggestions) {
|
|
@@ -290,7 +290,7 @@ export class DeliveryService {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
async send(botId, targetIdOrDraft, text, { signal } = {}) {
|
|
293
|
+
async send(botId, targetIdOrDraft, text, { signal, format = 'plain' } = {}) {
|
|
294
294
|
const id = botIdOf(botId);
|
|
295
295
|
const targetKey = typeof targetIdOrDraft === 'string'
|
|
296
296
|
? targetIdOf(targetIdOrDraft)
|
|
@@ -299,6 +299,9 @@ export class DeliveryService {
|
|
|
299
299
|
if (typeof text !== 'string' || !text.trim()) {
|
|
300
300
|
throw deliveryError('bad-request', 'Message text is required');
|
|
301
301
|
}
|
|
302
|
+
if (format !== 'plain' && format !== 'markdown') {
|
|
303
|
+
throw deliveryError('bad-request', 'Message format must be plain or markdown');
|
|
304
|
+
}
|
|
302
305
|
cancellation(signal);
|
|
303
306
|
const adapter = await this.#adapterFor(id);
|
|
304
307
|
try {
|
|
@@ -312,7 +315,10 @@ export class DeliveryService {
|
|
|
312
315
|
if (!target) throw deliveryError('unknown-target', 'Unknown target');
|
|
313
316
|
}
|
|
314
317
|
cancellation(signal);
|
|
315
|
-
await adapter.sendText(id, target, text, {
|
|
318
|
+
await adapter.sendText(id, target, text, {
|
|
319
|
+
signal,
|
|
320
|
+
...(format === 'markdown' ? { format } : {}),
|
|
321
|
+
});
|
|
316
322
|
return { sent: true };
|
|
317
323
|
} catch (error) {
|
|
318
324
|
if (signal?.aborted || error?.name === 'AbortError' || error?.code === 'ABORT_ERR') {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Verify the built Host hook against a built DSH's real JSONL persistence. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { createImHostPlugin } from '../lib/index.js';
|
|
10
|
+
import {
|
|
11
|
+
captureContextEnhancement, enhanceContextContent, INJECTED_CONTEXT_TAGS,
|
|
12
|
+
} from '../src/channels/shared/context-enhancement.mjs';
|
|
13
|
+
|
|
14
|
+
if (!process.argv[2]) throw new Error('Pass the path of a built DSH checkout.');
|
|
15
|
+
const harnessRoot = resolve(process.argv[2]);
|
|
16
|
+
const fromHarness = createRequire(resolve(harnessRoot, 'packages/session/session-persistence-jsonl/package.json'));
|
|
17
|
+
const load = (name) => import(pathToFileURL(fromHarness.resolve(name)).href);
|
|
18
|
+
const { Context } = await load('@deepseek-ai/cordis');
|
|
19
|
+
const { SESSION_FORMAT_VERSION } = await load('@deepseek-ai/dsh-session');
|
|
20
|
+
const { default: JsonlSessionPersistence } = await load('@deepseek-ai/dsh-session-persistence-jsonl');
|
|
21
|
+
assert.equal(SESSION_FORMAT_VERSION, 4, 'This regression verifies Session format V4.');
|
|
22
|
+
|
|
23
|
+
const directory = await mkdtemp(join(tmpdir(), 'dsh-im-context-'));
|
|
24
|
+
const ctx = new Context();
|
|
25
|
+
const source = { channel: 'feishu', senderName: 'Context test' };
|
|
26
|
+
const guidance = 'Reply briefly.';
|
|
27
|
+
const reply = `${INJECTED_CONTEXT_TAGS.replyOpen}${JSON.stringify({
|
|
28
|
+
authorName: 'Quoted author', content: 'Quoted text',
|
|
29
|
+
})}${INJECTED_CONTEXT_TAGS.replyClose}`;
|
|
30
|
+
|
|
31
|
+
function enhanced(content, fields = ['channel', 'senderName'], instructions = '') {
|
|
32
|
+
const snapshot = captureContextEnhancement({
|
|
33
|
+
botId: 'context-test',
|
|
34
|
+
getSettings: () => ({
|
|
35
|
+
group: { enabled: true, fields, guidance: instructions },
|
|
36
|
+
direct: { enabled: false, fields: [], guidance: '' },
|
|
37
|
+
}),
|
|
38
|
+
}, 'group');
|
|
39
|
+
return enhanceContextContent(content, snapshot, () => source);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function turnEvents(messages, turn = 1, offset = 0) {
|
|
43
|
+
return [
|
|
44
|
+
{ type: 'turn/start', data: { turn } },
|
|
45
|
+
{ type: 'step/start', data: { turn, step: 1 } },
|
|
46
|
+
...messages.map(data => ({ type: 'user/message', data, surfaceOp: 'append' })),
|
|
47
|
+
{ type: 'step/end', data: { turn, step: 1 } },
|
|
48
|
+
{ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } },
|
|
49
|
+
].map((event, index) => ({ ...event, seq: offset + index, time: offset + index + 1 }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cases = [
|
|
53
|
+
['plain', 'Question', ['user']],
|
|
54
|
+
['source', enhanced('Question'), ['user', 'notice']],
|
|
55
|
+
['guidance', enhanced('Question', [], guidance), ['user', 'instructions']],
|
|
56
|
+
['quote', `${reply}\n\nQuestion`, ['notice', 'user']],
|
|
57
|
+
['combined', enhanced(`${reply}\n\nQuestion`, ['channel'], guidance),
|
|
58
|
+
['notice', 'user', 'notice', 'instructions']],
|
|
59
|
+
['structured', enhanced([{ type: 'text', text: reply }, { type: 'text', text: 'Question' }]),
|
|
60
|
+
['notice', 'user', 'notice']],
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await ctx.plugin(JsonlSessionPersistence, { root: directory, compression: 'none' });
|
|
65
|
+
for (const [name, value] of Object.entries({
|
|
66
|
+
connection: {}, credentials: {}, typertGateway: { stream() {} },
|
|
67
|
+
sessionController: {}, workspaceController: {},
|
|
68
|
+
})) ctx.provide(name, value);
|
|
69
|
+
// Keep the real bundled context hook; unrelated channel connections stay idle.
|
|
70
|
+
const internals = Object.fromEntries([
|
|
71
|
+
'Feishu', 'Weixin', 'Dingtalk', 'Wecom', 'WecomApp', 'Qq', 'Slack',
|
|
72
|
+
'Telegram', 'Discord', 'Whatsapp', 'IMessage', 'Email', 'Matrix', 'Office',
|
|
73
|
+
].map(name => [`apply${name}`, async () => {}]));
|
|
74
|
+
const host = ctx.plugin(createImHostPlugin({
|
|
75
|
+
...internals,
|
|
76
|
+
installHostLanguage: () => {},
|
|
77
|
+
installSessionSyncCoordinator: () => {},
|
|
78
|
+
}));
|
|
79
|
+
await host.await();
|
|
80
|
+
|
|
81
|
+
for (const [id, content, expectedForms] of cases) {
|
|
82
|
+
const message = {
|
|
83
|
+
id: `${id}-user`, role: 'user',
|
|
84
|
+
content: typeof content === 'string' ? [{ type: 'text', text: content }] : content,
|
|
85
|
+
source: { kind: 'user', rpcId: `feishu-${id}` },
|
|
86
|
+
};
|
|
87
|
+
const decision = { kind: 'enter', messages: [message] };
|
|
88
|
+
const result = await ctx.waterfall('agent/pre-step', { agent: { session: { id } } },
|
|
89
|
+
async () => decision);
|
|
90
|
+
assert.deepEqual(result.messages.map(entry => entry.source.kind === 'user' ? 'user' : entry.source.form), expectedForms, id);
|
|
91
|
+
const human = result.messages.find(entry => entry.source.kind === 'user');
|
|
92
|
+
assert.equal(human.id, message.id);
|
|
93
|
+
assert.deepEqual(human.source, message.source);
|
|
94
|
+
assert.deepEqual(human.content, [{ type: 'text', text: 'Question' }]);
|
|
95
|
+
for (const context of result.messages.filter(entry => entry !== human)) {
|
|
96
|
+
assert.equal(context.source.kind, 'plugin:dsh-im', id);
|
|
97
|
+
assert.equal(Object.hasOwn(context.source, 'plugin'), false, id);
|
|
98
|
+
if (context.source.form === 'notice') assert.ok(context.source.summary.length > 0, id);
|
|
99
|
+
}
|
|
100
|
+
assert.equal(await ctx.waterfall('agent/pre-step', { agent: { session: { id } } }, async () => result), result);
|
|
101
|
+
|
|
102
|
+
const header = { version: SESSION_FORMAT_VERSION, id, createdAt: 1, isSeeded: false, delegationDepth: 0 };
|
|
103
|
+
const events = turnEvents(result.messages);
|
|
104
|
+
const writer = await ctx.sessionPersistence.create(header);
|
|
105
|
+
try { await writer.append(events); } finally { await writer.close(); }
|
|
106
|
+
// Reopen for a second turn, then verify both turns through a fresh read handle.
|
|
107
|
+
const resumed = await ctx.sessionPersistence.open(id, 'write');
|
|
108
|
+
const second = turnEvents([{ ...human, id: `${id}-second` }], 2, events.length);
|
|
109
|
+
try { await resumed.append(second); } finally { await resumed.close(); }
|
|
110
|
+
const reader = await ctx.sessionPersistence.open(id, 'read');
|
|
111
|
+
try {
|
|
112
|
+
assert.deepEqual((await reader.read()).events, [...events, ...second], id);
|
|
113
|
+
} finally { await reader.close(); }
|
|
114
|
+
console.log(`PASS ${id}: bundled hook, persistence, reopen and next turn`);
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
try { await ctx.fiber.dispose(); }
|
|
118
|
+
finally { await rm(directory, { recursive: true, force: true }); }
|
|
119
|
+
}
|
|
120
|
+
console.log(`Injected context verified against ${harnessRoot} (format V4).`);
|
|
@@ -628,7 +628,7 @@ export class FeishuRuntime {
|
|
|
628
628
|
});
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
-
async sendProactiveText(target, text, { signal } = {}) {
|
|
631
|
+
async sendProactiveText(target, text, { signal, format = 'plain' } = {}) {
|
|
632
632
|
if (!this.#status.ready || !this.#client) {
|
|
633
633
|
const error = new Error('飞书机器人尚未连接');
|
|
634
634
|
error.code = 'bot-not-connected';
|
|
@@ -646,13 +646,23 @@ export class FeishuRuntime {
|
|
|
646
646
|
error.code = 'invalid-target';
|
|
647
647
|
throw error;
|
|
648
648
|
}
|
|
649
|
+
if (format !== 'plain' && format !== 'markdown') {
|
|
650
|
+
const error = new TypeError('Message format must be plain or markdown');
|
|
651
|
+
error.code = 'bad-request';
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
649
654
|
signal?.throwIfAborted();
|
|
655
|
+
// Use the same native Markdown element as chat, without opening a stream
|
|
656
|
+
// or retrying as plain text after a possibly accepted delivery.
|
|
657
|
+
const content = format === 'markdown'
|
|
658
|
+
? { schema: '2.0', body: { elements: [{ tag: 'markdown', content: text }] } }
|
|
659
|
+
: { text };
|
|
650
660
|
const response = await this.#client.im.v1.message.create({
|
|
651
661
|
params: { receive_id_type: receiveIdType },
|
|
652
662
|
data: {
|
|
653
663
|
receive_id: receiveId,
|
|
654
|
-
msg_type: 'text',
|
|
655
|
-
content: JSON.stringify(
|
|
664
|
+
msg_type: format === 'markdown' ? 'interactive' : 'text',
|
|
665
|
+
content: JSON.stringify(content),
|
|
656
666
|
},
|
|
657
667
|
});
|
|
658
668
|
if (response?.code && response.code !== 0) {
|
|
@@ -11,6 +11,7 @@ export default {
|
|
|
11
11
|
"模型图片总量上限 (MB)": "Model total image limit (MB)",
|
|
12
12
|
"每条消息最多图片数": "Maximum images per message",
|
|
13
13
|
"图片输入": "Image input",
|
|
14
|
+
"查看图片输入说明": "View image input details",
|
|
14
15
|
"适用于支持图片的聊天渠道。原图按附件保留时长保存,发送给模型的副本会自动缩放或压缩;无法直接发送时交给模型按文件处理。": "Applies to chat channels that support images. Originals follow attachment retention settings. Model copies are resized or compressed automatically; images that cannot be sent directly are provided as files.",
|
|
15
16
|
"保存图片设置": "Save image settings",
|
|
16
17
|
"正在读取图片设置…": "Loading image settings…"
|
|
@@ -252,7 +252,7 @@ export function splitLeadingInjectedContext(text, options = {}) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
|
-
* Build the
|
|
255
|
+
* Build one context message with the producer-owned source kind required by V4.
|
|
256
256
|
* @param block - one parsed block.
|
|
257
257
|
* @param newId - identity factory for the new message.
|
|
258
258
|
* @param plugin - source plugin name recorded on it.
|
|
@@ -260,8 +260,8 @@ export function splitLeadingInjectedContext(text, options = {}) {
|
|
|
260
260
|
*/
|
|
261
261
|
function contextMessage(block, newId, plugin) {
|
|
262
262
|
const source = block.form === 'notice' && typeof block.summary === 'string'
|
|
263
|
-
? { kind:
|
|
264
|
-
: { kind:
|
|
263
|
+
? { kind: `plugin:${plugin}`, form: 'notice', summary: block.summary }
|
|
264
|
+
: { kind: `plugin:${plugin}`, form: block.form };
|
|
265
265
|
return {
|
|
266
266
|
id: newId(),
|
|
267
267
|
role: 'user',
|