dsh-theone 0.3.17 → 0.3.18

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/dist/client.js CHANGED
@@ -544,7 +544,7 @@ function apply(ctx) {
544
544
  const prepared = await fetch("/api/theone/gateway/prepare", {
545
545
  method: "POST",
546
546
  headers: { "content-type": "application/json" },
547
- body: JSON.stringify({ sessionId: id }),
547
+ body: JSON.stringify({ sessionId: id, locale: localeSnapshot().active }),
548
548
  signal: lifetime.signal
549
549
  });
550
550
  if (!prepared.ok) throw new Error("Global gateway preparation failed");
package/dist/index.d.ts CHANGED
@@ -59,6 +59,11 @@ declare module '@deepseek-ai/dsh-llm' {
59
59
  contextId: string;
60
60
  related: string[];
61
61
  };
62
+ 'theone-welcome': {
63
+ kind: 'theone-welcome';
64
+ form: 'notice';
65
+ locale: string;
66
+ };
62
67
  }
63
68
  }
64
69
  export declare function viaModel(selection: ModelSelection): string;
@@ -187,6 +192,12 @@ export default class TheOne extends Service {
187
192
  private similarCorrections;
188
193
  /** The misrouted message, handed to the right topic with the user's correction. */
189
194
  private correctedInput;
195
+ /**
196
+ * DSH treats a session that never had a turn as blank, and a blank session outside any workspace
197
+ * locks its input until a workspace is chosen. The main chat belongs to no workspace, so a new one
198
+ * opens with a short welcome turn, which also tells the user how it works. No model is called.
199
+ */
200
+ welcomeGateway(id: string, locale: string): void;
190
201
  /** Topic → when main chat last answered in it; quick alternation between two topics links them. */
191
202
  private lastRoute?;
192
203
  private learnFromRoute;
package/dist/index.js CHANGED
@@ -82,9 +82,21 @@ function descriptorJson(context, budget) {
82
82
  const TERMS_PROMPT = `你在帮话题路由器从用户的更正中学习。用户确认这条 message 属于 rightTopic(而不是 wrongTopic)。
83
83
  从 message 原文中挑出最多 4 个能把它和 rightTopic 联系起来、又能和 wrongTopic 区分开的词语:项目名、术语、产品、人名、文件名等,每个 2–12 个字,必须在 message 中原样出现。
84
84
  不要选泛泛的词(如「帮我」「这个」「问题」),不要选 wrongTopic 的名字。只输出 JSON 字符串数组,例如 ["消融实验","第三章"]。`;
85
+ /** The first words of a new main chat, in the interface language. */
86
+ function welcomeText(locale) {
87
+ return locale.toLowerCase().startsWith('zh')
88
+ ? '你好!以后所有的事都在这里聊就行。TheOne 会判断每句话属于哪件事,交给那件事自己的后台会话去做,过程原样显示在这里。你以前在 DSH 里聊过的内容,会整理到左侧的「话题工作区」。'
89
+ : 'Hi! From now on, just talk here about anything. TheOne works out which topic each message belongs to, hands it to that topic\'s own background session, and shows the work right here. Your earlier DSH conversations are organized under **Topic workspaces** in the sidebar.';
90
+ }
85
91
  /** Why a message went where it did, as a short phrase for the "show every decision" notice. */
86
- function reasonLabel(reason) {
87
- const fixed = {
92
+ function reasonLabel(reason, english = false) {
93
+ const fixed = english ? {
94
+ 'steering': 'added during the reply', 'short-continuation': 'continuing', 'attachment-only': 'attachment', 'correction': 'your correction',
95
+ 'explicit-new-topic': 'new topic', 'no-history-evidence': 'new question', 'entity-or-keyword': 'mentions this topic',
96
+ 'keyword-only-switch': 'mentions this topic', 'current-reference': 'same topic', 'combined-contexts': 'combines topics',
97
+ 'insufficient-evidence': 'no matching topic', 'multiple-contexts': 'several topics fit', 'weak-keyword-match': 'new question', 'no-history-match': 'new question',
98
+ 'CATALOG_NOT_READY': 'history still being catalogued', 'CATALOG_REVIEW_LIMIT': 'no clearly related topic', 'HISTORY_SEARCH_UNAVAILABLE': 'history search unavailable',
99
+ } : {
88
100
  'steering': '回复中补充', 'short-continuation': '接着说', 'attachment-only': '附件', 'correction': '按你的更正',
89
101
  'explicit-new-topic': '明确的新话题', 'no-history-evidence': '新的问题', 'entity-or-keyword': '提到了这个话题',
90
102
  'keyword-only-switch': '提到了这个话题', 'current-reference': '接着当前话题', 'combined-contexts': '结合多个话题',
@@ -94,7 +106,7 @@ function reasonLabel(reason) {
94
106
  if (fixed[reason])
95
107
  return fixed[reason];
96
108
  if (reason.startsWith('router-fallback:'))
97
- return '分类暂不可用,按规则判断';
109
+ return english ? 'classifier unavailable, routed by rules' : '分类暂不可用,按规则判断';
98
110
  const text = reason.replace(/\s+/g, ' ').trim();
99
111
  return text.length > 60 ? text.slice(0, 59) + '…' : text;
100
112
  }
@@ -334,6 +346,9 @@ export default class TheOne extends Service {
334
346
  if (decision.kind === 'reject' || (provider !== 'theone' && !this.store.isPinnedGateway(agent.id)))
335
347
  return decision;
336
348
  signal.throwIfAborted();
349
+ // The welcome turn of a new main chat has nothing to route.
350
+ if (!decision.messages.some(message => message.source.kind === 'user') && decision.messages.some(message => message.source.kind === 'theone-welcome'))
351
+ return decision;
337
352
  // Know main chat's model choice before routing, so this very message is classified with it.
338
353
  const pickedNow = selected?.signal === signal ? selected.model : undefined;
339
354
  if (provider === 'theone')
@@ -495,8 +510,10 @@ export default class TheOne extends Service {
495
510
  const mark = { KEEP: '·', MOUNT: '→', SWAP: '→', CREATE: '+', CLARIFY: '?' }[route.decision.action];
496
511
  const notice = this.config.routeNotice ?? 'switch';
497
512
  // Showing every decision also says why it was made.
498
- const why = notice === 'all' ? ` · ${reasonLabel(route.decision.reason)}` : '';
499
- const summary = `${mark} ${titleOf(route.decision.contextId) ?? '请补充话题'}${references.length ? ` · 参考:${references.join('、')}` : ''}${why}`.slice(0, 160);
513
+ // The notice speaks the language the message was written in.
514
+ const english = !!text.trim() && !/[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af]/.test(text);
515
+ const why = notice === 'all' ? ` · ${reasonLabel(route.decision.reason, english)}` : '';
516
+ const summary = `${mark} ${titleOf(route.decision.contextId) ?? (english ? 'needs a topic' : '请补充话题')}${references.length ? (english ? ` · reference: ${references.join(', ')}` : ` · 参考:${references.join('、')}`) : ''}${why}`.slice(0, 160);
500
517
  const switched = route.decision.action === 'MOUNT' || route.decision.action === 'SWAP' || route.decision.action === 'CREATE';
501
518
  if (notice === 'hidden' || (notice === 'switch' && !switched))
502
519
  return decision;
@@ -584,6 +601,8 @@ export default class TheOne extends Service {
584
601
  }
585
602
  this.store.rememberGateway(this.config.gatewayKey, id);
586
603
  await scope.workspaceRegistry.unarchiveSession(id);
604
+ const locale = 'locale' in value && typeof value.locale === 'string' ? value.locale.slice(0, 16) : 'en';
605
+ this.welcomeGateway(id, locale);
587
606
  return Response.json({ prepared: true, workspaceId: null });
588
607
  } }));
589
608
  });
@@ -1326,6 +1345,17 @@ export default class TheOne extends Service {
1326
1345
  yield* running.stream(options.signal);
1327
1346
  return;
1328
1347
  }
1348
+ // Other plugins may add their own context after it; the welcome is the newest input from either side.
1349
+ const latest = [...options.messages].reverse().find(message => message.role === 'user' && 'source' in message
1350
+ && (message.source?.kind === 'user' || message.source?.kind === 'theone-welcome'));
1351
+ if (latest && 'source' in latest && latest.source?.kind === 'theone-welcome') {
1352
+ const text = welcomeText(latest.source.locale);
1353
+ yield { type: 'block-start', index: 0, blockType: 'text' };
1354
+ yield { type: 'text-delta', index: 0, text };
1355
+ yield { type: 'block-end', index: 0, block: { type: 'text', text } };
1356
+ yield { type: 'finish', reason: { kind: 'stop' } };
1357
+ return;
1358
+ }
1329
1359
  const input = [...options.messages].reverse().find((message) => message.role === 'user' && 'source' in message && message.source?.kind === 'user');
1330
1360
  if (!input)
1331
1361
  throw new Error('TheOne requires a session-backed user input');
@@ -1545,6 +1575,19 @@ export default class TheOne extends Service {
1545
1575
  return createUserMessage({ source: correction.source, content: [...event.data.content,
1546
1576
  { type: 'text', text: `\n\n(这条消息先前被分到了别的话题,用户更正后交给这里处理${note ? `。用户的更正:${note}` : ''})` }] });
1547
1577
  }
1578
+ /**
1579
+ * DSH treats a session that never had a turn as blank, and a blank session outside any workspace
1580
+ * locks its input until a workspace is chosen. The main chat belongs to no workspace, so a new one
1581
+ * opens with a short welcome turn, which also tells the user how it works. No model is called.
1582
+ */
1583
+ welcomeGateway(id, locale) {
1584
+ const agent = this.ctx.agents.get(SessionId(id));
1585
+ if (!agent || agent.status !== 'idle' || agent.inbox.nextTurn.length || agent.inbox.nextStep.length)
1586
+ return;
1587
+ if (agent.session.snapshotEvents().some(event => event.type === 'turn/start'))
1588
+ return;
1589
+ agent.followup(createUserMessage({ source: { kind: 'theone-welcome', form: 'notice', locale }, content: [{ type: 'text', text: 'TheOne' }] }));
1590
+ }
1548
1591
  /** Topic → when main chat last answered in it; quick alternation between two topics links them. */
1549
1592
  lastRoute;
1550
1593
  learnFromRoute(decision, previous, now = Date.now()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-theone",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Automatic context routing for DeepSeek Harness",