@xmanrui/dsh-im 4.19.0 → 4.19.2
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 +18 -4
- package/README.md +18 -4
- package/lib/client.js +188 -10
- package/lib/index.js +262 -261
- package/package.json +5 -1
- package/plugin-src/client/bot-alias.js +78 -1
- package/plugin-src/client/index.js +15 -0
- package/plugin-src/client/interface-language.js +89 -0
- package/plugin-src/client/styles.js +17 -8
- package/plugin-src/host/channels/shared/startup.mjs +23 -0
- package/plugin-src/host/host-language-rpc.mjs +71 -0
- package/plugin-src/host/host-language.mjs +157 -0
- package/plugin-src/host/index.mjs +15 -2
- package/plugin-src/host/session-sync-coordinator.mjs +14 -13
- package/scripts/verify-interface-language.mjs +333 -0
- package/src/channels/discord/discord-runtime.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +219 -262
- package/src/channels/shared/i18n-en/discord.mjs +2 -0
- package/src/channels/shared/i18n-en/telegram.mjs +5 -0
- package/src/channels/shared/i18n.mjs +46 -3
- package/src/channels/shared/interface-language-store.mjs +127 -0
- package/src/channels/shared/interface-language.mjs +51 -0
- package/src/channels/shared/session-sync-registry.mjs +16 -28
- package/src/channels/shared/token-bot-controller.mjs +26 -0
- package/src/channels/telegram/telegram-runtime.mjs +75 -13
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run the built plugin through an original DSH CLI and prove that bot messages
|
|
3
|
+
* follow the DSH interface language (issue #185).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors scripts/verify-lan-management.mjs: an isolated, empty home, the
|
|
6
|
+
* unmodified CLI, and real HTTP against the public /api carrier. Nothing is
|
|
7
|
+
* mocked — the assertions are on what the Host resolves from DSH's own
|
|
8
|
+
* user-settings document.
|
|
9
|
+
*
|
|
10
|
+
* Set DSH_IM_TELEGRAM_TOKEN to additionally bind a real bot and assert the
|
|
11
|
+
* command menu Telegram itself stores. That step is skipped without a token,
|
|
12
|
+
* and it restores the bot's original menu when it finishes.
|
|
13
|
+
*/
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { access, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { request as httpRequest } from 'node:http';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { dirname, join, resolve } from 'node:path';
|
|
20
|
+
|
|
21
|
+
const harnessRoot = process.argv[2];
|
|
22
|
+
if (!harnessRoot || process.argv.includes('--help')) {
|
|
23
|
+
console.log('Usage: node scripts/verify-interface-language.mjs /path/to/built/deepseek-harness');
|
|
24
|
+
console.log(' DSH_IM_TELEGRAM_TOKEN=<token> node scripts/verify-interface-language.mjs ... (adds the live menu check)');
|
|
25
|
+
process.exit(harnessRoot ? 0 : 1);
|
|
26
|
+
}
|
|
27
|
+
const pluginRoot = resolve(import.meta.dirname, '..');
|
|
28
|
+
await access(join(pluginRoot, 'lib/index.js'));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Accept either a built Harness checkout or an installed @deepseek-ai/dsh
|
|
32
|
+
* package, so this runs against a release as well as a working tree.
|
|
33
|
+
*/
|
|
34
|
+
async function resolveHarness(root) {
|
|
35
|
+
const layouts = [
|
|
36
|
+
{ kind: 'checkout', cli: 'apps/cli/lib/bin.js', base: 'packages/bundle/base', webApp: 'packages/bundle/web-app' },
|
|
37
|
+
{ kind: 'package', cli: 'lib/bin.js', base: 'node_modules/@deepseek-ai/dsh-base', webApp: 'node_modules/@deepseek-ai/dsh-web-app' },
|
|
38
|
+
];
|
|
39
|
+
for (const layout of layouts) {
|
|
40
|
+
const paths = {
|
|
41
|
+
kind: layout.kind,
|
|
42
|
+
cli: resolve(root, layout.cli),
|
|
43
|
+
base: resolve(root, layout.base),
|
|
44
|
+
webApp: resolve(root, layout.webApp),
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
await Promise.all([access(paths.cli), access(paths.base), access(paths.webApp)]);
|
|
48
|
+
return paths;
|
|
49
|
+
} catch {
|
|
50
|
+
// Try the next known layout.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`no DSH CLI found under ${root}: expected apps/cli/lib/bin.js or lib/bin.js`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const harness = await resolveHarness(harnessRoot);
|
|
57
|
+
const cli = harness.cli;
|
|
58
|
+
const botToken = process.env.DSH_IM_TELEGRAM_TOKEN;
|
|
59
|
+
let telegramMenu;
|
|
60
|
+
const directory = await mkdtemp(join(tmpdir(), 'dsh-im-language-test-'));
|
|
61
|
+
const home = join(directory, 'home');
|
|
62
|
+
const profile = join(home, 'profiles/web');
|
|
63
|
+
const settingsPath = join(home, 'settings.yaml');
|
|
64
|
+
const mirrorPath = join(home, 'integrations/dsh-im/interface-language.json');
|
|
65
|
+
const results = [];
|
|
66
|
+
let child;
|
|
67
|
+
|
|
68
|
+
function request(url, { method = 'GET', headers = {}, body } = {}) {
|
|
69
|
+
return new Promise((resolveRequest, reject) => {
|
|
70
|
+
const req = httpRequest({
|
|
71
|
+
hostname: '127.0.0.1', port: url.port, path: `${url.pathname}${url.search}`,
|
|
72
|
+
method, headers: { host: url.host, ...headers },
|
|
73
|
+
}, res => {
|
|
74
|
+
let text = '';
|
|
75
|
+
res.setEncoding('utf8');
|
|
76
|
+
res.on('data', chunk => { text += chunk; });
|
|
77
|
+
res.on('error', reject);
|
|
78
|
+
res.on('end', () => resolveRequest({ status: res.statusCode, headers: res.headers, body: text }));
|
|
79
|
+
});
|
|
80
|
+
req.on('error', reject);
|
|
81
|
+
req.setTimeout(15_000, () => req.destroy(new Error('HTTP request timed out')));
|
|
82
|
+
req.end(body);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function start() {
|
|
87
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => (
|
|
88
|
+
!/^DSH_/i.test(key) && !/(?:KEY|SECRET|TOKEN|PASSWORD|PROXY)/i.test(key)
|
|
89
|
+
)));
|
|
90
|
+
child = spawn(process.execPath, [cli, 'web', '--no-open', '--host', '127.0.0.1', '--port', '0'], {
|
|
91
|
+
cwd: directory,
|
|
92
|
+
env: { ...env, DSH_HOME: home, DSH_AGENTS_HOME: join(directory, '.agents'),
|
|
93
|
+
DSH_TELEMETRY_DISABLED: '1', SSH_CONNECTION: '', SSH_TTY: '' },
|
|
94
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
95
|
+
});
|
|
96
|
+
return new Promise((resolveStart, reject) => {
|
|
97
|
+
let output = '';
|
|
98
|
+
const timer = setTimeout(() => finish(new Error('DSH startup timed out')), 60_000);
|
|
99
|
+
let settled = false;
|
|
100
|
+
function finish(error, url) {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
if (error) reject(new Error(`${error.message}\n${output.replace(/([?&]token=)[^\s)]+/g, '$1<redacted>')}`));
|
|
105
|
+
else resolveStart(new URL(url));
|
|
106
|
+
}
|
|
107
|
+
function append(chunk) {
|
|
108
|
+
output = `${output}${chunk}`.slice(-100_000);
|
|
109
|
+
const match = /dsh web: (http:\/\/[^\s]+)/.exec(output);
|
|
110
|
+
if (match) finish(null, match[1]);
|
|
111
|
+
}
|
|
112
|
+
child.stdout.on('data', append);
|
|
113
|
+
child.stderr.on('data', append);
|
|
114
|
+
child.once('error', error => finish(error));
|
|
115
|
+
child.once('exit', code => finish(new Error(`DSH exited before readiness: ${code}`)));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function stop() {
|
|
120
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
121
|
+
const stopped = new Promise(resolveStop => child.once('exit', resolveStop));
|
|
122
|
+
child.kill('SIGTERM');
|
|
123
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 10_000);
|
|
124
|
+
try { await stopped; } finally { clearTimeout(timer); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function login(launchUrl) {
|
|
128
|
+
const response = await request(launchUrl);
|
|
129
|
+
assert.equal(response.status, 303, 'Harness must exchange its launch token for a browser cookie');
|
|
130
|
+
const cookie = response.headers['set-cookie']?.map(value => value.split(';', 1)[0]).join('; ');
|
|
131
|
+
assert.ok(cookie, 'Harness did not issue a browser cookie');
|
|
132
|
+
const browser = { origin: launchUrl.origin, cookie };
|
|
133
|
+
// HTTP readiness precedes asynchronous plugin activation, notably after
|
|
134
|
+
// restart. Wait only for the route to appear; other failures stay fatal.
|
|
135
|
+
await waitFor('the plugin language route becomes ready after HTTP startup', async () => {
|
|
136
|
+
try {
|
|
137
|
+
const result = await rpc(browser, 'dsh-im-language', 'settings.language.get');
|
|
138
|
+
return result.ok === true;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error.code === 'ERR_ASSERTION' && error.actual === 404) return false;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return browser;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function rpc(browser, channel, method, payload = {}) {
|
|
148
|
+
const response = await request(new URL(`/api/dsh-im/${channel}`, browser.origin), {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'content-type': 'application/json', origin: browser.origin, cookie: browser.cookie },
|
|
151
|
+
body: JSON.stringify({ type: 'client-request', rpcId: 'language-test',
|
|
152
|
+
method: `dsh-im/${channel}`, payload: { method, payload } }),
|
|
153
|
+
});
|
|
154
|
+
assert.equal(response.status, 200, `${channel}/${method} -> HTTP ${response.status}: ${response.body.slice(0, 300)}`);
|
|
155
|
+
return JSON.parse(response.body).result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Rewrite only the `locale` section, leaving the rest of the document alone. */
|
|
159
|
+
async function selectInterfaceLanguage(value) {
|
|
160
|
+
let raw = '';
|
|
161
|
+
try { raw = await readFile(settingsPath, 'utf8'); } catch { raw = ''; }
|
|
162
|
+
const withoutLocale = raw.replace(/(^|\n)locale:\n(?:[ \t]+.*\n?)*/g, '$1').trimEnd();
|
|
163
|
+
await writeFile(settingsPath, value === null
|
|
164
|
+
? `${withoutLocale}\n`
|
|
165
|
+
: `${withoutLocale}\nlocale:\n preference: ${value}\n`, 'utf8');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function waitFor(describe, predicate, { attempts = 60, delayMs = 250 } = {}) {
|
|
169
|
+
let last;
|
|
170
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
171
|
+
last = await predicate();
|
|
172
|
+
if (last) return last;
|
|
173
|
+
await new Promise(sleep => setTimeout(sleep, delayMs));
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`${describe} never held (last: ${JSON.stringify(last)?.slice(0, 300)})`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function record(check, detail) {
|
|
179
|
+
results.push({ check, detail: detail ?? '', result: 'PASS' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const HAN = /[\p{Script=Han}]/u;
|
|
183
|
+
|
|
184
|
+
async function telegram(method, payload) {
|
|
185
|
+
const response = await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: { 'content-type': 'application/json' },
|
|
188
|
+
body: JSON.stringify(payload ?? {}),
|
|
189
|
+
});
|
|
190
|
+
const parsed = await response.json();
|
|
191
|
+
assert.equal(parsed.ok, true, `telegram ${method} failed`);
|
|
192
|
+
return parsed.result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await mkdir(profile, { recursive: true });
|
|
197
|
+
const packages = {
|
|
198
|
+
'@deepseek-ai/dsh-base': harness.base,
|
|
199
|
+
'@deepseek-ai/dsh-web-app': harness.webApp,
|
|
200
|
+
'@xmanrui/dsh-im': pluginRoot,
|
|
201
|
+
};
|
|
202
|
+
for (const [name, path] of Object.entries(packages)) {
|
|
203
|
+
const target = join(profile, 'node_modules', name);
|
|
204
|
+
await mkdir(dirname(target), { recursive: true });
|
|
205
|
+
await symlink(path, target, 'dir');
|
|
206
|
+
}
|
|
207
|
+
await writeFile(join(profile, 'cordis.yml'), '[]\n');
|
|
208
|
+
await writeFile(join(profile, 'package.json'), JSON.stringify({
|
|
209
|
+
name: 'dsh-im-language-test', private: true,
|
|
210
|
+
dependencies: Object.fromEntries(Object.entries(packages).map(([name, path]) => [name, `link:${path}`])),
|
|
211
|
+
dsh: { profile: { bundles: Object.keys(packages) } },
|
|
212
|
+
}));
|
|
213
|
+
|
|
214
|
+
// A Chinese interface selection, exactly as DSH's Language row stores it.
|
|
215
|
+
await selectInterfaceLanguage('zh');
|
|
216
|
+
let browser = await login(await start());
|
|
217
|
+
const language = (method, payload) => rpc(browser, 'dsh-im-language', method, payload);
|
|
218
|
+
|
|
219
|
+
const initial = await language('settings.language.get');
|
|
220
|
+
assert.equal(initial.ok, true, JSON.stringify(initial));
|
|
221
|
+
assert.deepEqual(initial.value, { language: 'zh', tag: 'zh', source: 'settings', pinned: false },
|
|
222
|
+
'the plugin must read DSH\'s own locale preference');
|
|
223
|
+
record('DSH locale namespace is registered and read Host-side', JSON.stringify(initial.value));
|
|
224
|
+
|
|
225
|
+
await selectInterfaceLanguage('en');
|
|
226
|
+
const switched = await waitFor('the bot language follows DSH to English', async () => {
|
|
227
|
+
const snapshot = await language('settings.language.get');
|
|
228
|
+
return snapshot.value?.language === 'en' ? snapshot.value : null;
|
|
229
|
+
});
|
|
230
|
+
assert.deepEqual(switched, { language: 'en', tag: 'en', source: 'settings', pinned: false });
|
|
231
|
+
record('switching DSH\'s interface language switches the bot language live', 'no restart');
|
|
232
|
+
|
|
233
|
+
const mirrored = await language('settings.language.mirror', { locale: 'zh-CN' });
|
|
234
|
+
assert.deepEqual(mirrored.value, { language: 'en', tag: 'en', source: 'settings', pinned: false },
|
|
235
|
+
'the mirror must never outrank an explicit selection');
|
|
236
|
+
record('an explicit DSH selection outranks the mirrored interface locale', 'settings > mirror');
|
|
237
|
+
|
|
238
|
+
await selectInterfaceLanguage(null);
|
|
239
|
+
const fallback = await waitFor('the mirror applies once the selection is cleared', async () => {
|
|
240
|
+
const snapshot = await language('settings.language.get');
|
|
241
|
+
return snapshot.value?.source === 'mirror' ? snapshot.value : null;
|
|
242
|
+
});
|
|
243
|
+
assert.deepEqual(fallback, { language: 'zh', tag: 'zh-CN', source: 'mirror', pinned: false });
|
|
244
|
+
record('clearing the selection falls back to the mirrored interface locale', 'mirror > default');
|
|
245
|
+
|
|
246
|
+
// The reported case: an English interface that DSH never stored, because it
|
|
247
|
+
// came from the browser's language list rather than the Language row.
|
|
248
|
+
const browserEnglish = await language('settings.language.mirror', { locale: 'en-GB' });
|
|
249
|
+
assert.deepEqual(browserEnglish.value, { language: 'en', tag: 'en-GB', source: 'mirror', pinned: false });
|
|
250
|
+
assert.deepEqual(JSON.parse(await readFile(mirrorPath, 'utf8')),
|
|
251
|
+
{ version: 1, interfaceLanguage: 'en-GB' });
|
|
252
|
+
record('a browser-derived English interface reaches the Host and is persisted', 'issue #185 case');
|
|
253
|
+
|
|
254
|
+
for (const [method, payload] of [
|
|
255
|
+
['settings.language.mirror', { locale: 'not a tag' }],
|
|
256
|
+
['settings.language.mirror', {}],
|
|
257
|
+
['settings.language.get', { locale: 'en' }],
|
|
258
|
+
['settings.language.unknown', {}],
|
|
259
|
+
]) {
|
|
260
|
+
const refused = await language(method, payload);
|
|
261
|
+
assert.equal(refused.ok, false, `${method} must be refused`);
|
|
262
|
+
assert.equal(refused.error.code, 'bad-request');
|
|
263
|
+
}
|
|
264
|
+
record('the language route refuses malformed payloads', '4 cases');
|
|
265
|
+
|
|
266
|
+
if (botToken) {
|
|
267
|
+
telegramMenu = await telegram('getMyCommands');
|
|
268
|
+
const bound = await rpc(browser, 'telegram', 'bot.bind-credentials', { token: botToken });
|
|
269
|
+
assert.equal(bound.ok, true, 'the bot must bind');
|
|
270
|
+
const botId = bound.value.bots.at(-1).botId;
|
|
271
|
+
await waitFor('the bot connects', async () => {
|
|
272
|
+
const status = await rpc(browser, 'telegram', 'connection.status');
|
|
273
|
+
return status.value?.bots?.find(item => item.botId === botId)?.connected ? true : null;
|
|
274
|
+
});
|
|
275
|
+
const english = await waitFor('Telegram stores the English menu', async () => {
|
|
276
|
+
const commands = await telegram('getMyCommands');
|
|
277
|
+
const help = commands.find(item => item.command === 'help');
|
|
278
|
+
return help && !HAN.test(help.description) ? commands : null;
|
|
279
|
+
});
|
|
280
|
+
record('Telegram stores the English command menu while DSH is English', `${english.length} commands`);
|
|
281
|
+
|
|
282
|
+
await selectInterfaceLanguage('zh');
|
|
283
|
+
const chinese = await waitFor('Telegram stores the Chinese menu after the switch', async () => {
|
|
284
|
+
const commands = await telegram('getMyCommands');
|
|
285
|
+
const help = commands.find(item => item.command === 'help');
|
|
286
|
+
return help && HAN.test(help.description) ? commands : null;
|
|
287
|
+
});
|
|
288
|
+
assert.equal(chinese.length, english.length, 'the same catalog in another language');
|
|
289
|
+
const still = await rpc(browser, 'telegram', 'connection.status');
|
|
290
|
+
assert.equal(still.value.bots.find(item => item.botId === botId).connected, true,
|
|
291
|
+
'the command menu must be re-sent without reconnecting the bot');
|
|
292
|
+
record('switching language re-sends the Telegram menu with no reconnect', `${chinese.length} commands`);
|
|
293
|
+
await selectInterfaceLanguage(null);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// A restart must keep answering in the mirrored language, before any browser
|
|
297
|
+
// connects, and must resolve it before the first bot registers its menu.
|
|
298
|
+
await stop();
|
|
299
|
+
const restarted = await login(await start());
|
|
300
|
+
browser = restarted;
|
|
301
|
+
const afterRestart = await rpc(restarted, 'dsh-im-language', 'settings.language.get');
|
|
302
|
+
assert.deepEqual(afterRestart.value, { language: 'en', tag: 'en-GB', source: 'mirror', pinned: false });
|
|
303
|
+
record('the mirrored language survives a Host restart', 'resolved before channels start');
|
|
304
|
+
|
|
305
|
+
// Back-compatibility: an operator pin keeps winning and ignores DSH entirely.
|
|
306
|
+
await stop();
|
|
307
|
+
await writeFile(join(profile, 'cordis.patch.yml'),
|
|
308
|
+
'- id: xmanrui-dsh-im\n config:\n language: zh-CN\n');
|
|
309
|
+
await selectInterfaceLanguage('en');
|
|
310
|
+
const pinnedBrowser = await login(await start());
|
|
311
|
+
const pinned = await rpc(pinnedBrowser, 'dsh-im-language', 'settings.language.get');
|
|
312
|
+
assert.deepEqual(pinned.value, { language: 'zh', tag: 'zh-CN', source: 'config', pinned: true },
|
|
313
|
+
'a configured language must ignore DSH\'s interface language');
|
|
314
|
+
record('an operator-configured language still wins', 'existing setups unchanged');
|
|
315
|
+
|
|
316
|
+
console.table(results);
|
|
317
|
+
console.log(`Passed ${results.length} real HTTP checks against an unmodified DSH CLI (${harness.kind} layout).`);
|
|
318
|
+
console.log(botToken
|
|
319
|
+
? 'The live Telegram menu was exercised and the bot\'s original menu was restored.'
|
|
320
|
+
: 'Set DSH_IM_TELEGRAM_TOKEN to additionally assert the menu Telegram itself stores.');
|
|
321
|
+
console.log('The temporary home contains no bot credentials and is removed after the server stops.');
|
|
322
|
+
} finally {
|
|
323
|
+
await stop();
|
|
324
|
+
if (botToken && telegramMenu !== undefined) {
|
|
325
|
+
try {
|
|
326
|
+
if (telegramMenu.length > 0) await telegram('setMyCommands', { commands: telegramMenu });
|
|
327
|
+
else await telegram('deleteMyCommands');
|
|
328
|
+
} catch {
|
|
329
|
+
console.error('could not restore the bot\'s original command menu');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
await rm(directory, { recursive: true, force: true });
|
|
333
|
+
}
|
|
@@ -149,7 +149,7 @@ async function sendThreadUncertainNotice(api, normalized, signal) {
|
|
|
149
149
|
await api.createMessage({
|
|
150
150
|
channelId: normalized.replyTarget.channelId,
|
|
151
151
|
replyToMessageId: normalized.replyTarget.replyToMessageId,
|
|
152
|
-
content: 'Thread 创建结果暂时无法确认。若已创建,请在对应 Thread 中重试;若未创建,请稍后重新 @机器人。',
|
|
152
|
+
content: t('Thread 创建结果暂时无法确认。若已创建,请在对应 Thread 中重试;若未创建,请稍后重新 @机器人。'),
|
|
153
153
|
signal,
|
|
154
154
|
});
|
|
155
155
|
} catch (error) {
|