dsh-sidecard-ask 2.1.7 → 2.1.9

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/index.js CHANGED
@@ -45,7 +45,7 @@ export const name = 'dsh-sidecard-ask'
45
45
  export const inject = ['webServer']
46
46
 
47
47
  /** Version of this plugin (kept in step with package.json by test/verify.mjs). */
48
- export const PLUGIN_VERSION = '2.1.7'
48
+ export const PLUGIN_VERSION = '2.1.9'
49
49
 
50
50
  /** Route prefix of the plugin's own API. */
51
51
  export const ROUTE_PREFIX = '/sidecard-ask/api'
@@ -127,6 +127,9 @@ export const DEFAULT_CONFIG = {
127
127
  * floating layer is used either way.
128
128
  */
129
129
  dockRail: true,
130
+ // 2.1.9: expand the floating layer when an answer settles, so the answer is
131
+ // visible in the side card instead of hiding behind the capsule pill.
132
+ autoExpandOnDone: true,
130
133
  /**
131
134
  * One-click quick actions rendered beside the selection trigger.
132
135
  *
@@ -534,7 +537,7 @@ export function sanitizeLayer(raw, problems = []) {
534
537
  if (value !== null && (value === '' || looksLikeEmail(value))) out.feedbackSmtpFrom = value
535
538
  else problems.push('feedbackSmtpFrom: 必须是合法邮箱或空字符串,已忽略')
536
539
  }
537
- for (const boolKey of ['showInUnclassified', 'notifyOnDone', 'contextMenu', 'followUpSuggestions', 'dockRail']) {
540
+ for (const boolKey of ['showInUnclassified', 'notifyOnDone', 'contextMenu', 'followUpSuggestions', 'dockRail', 'autoExpandOnDone']) {
538
541
  if (raw[boolKey] === undefined) continue
539
542
  if (typeof raw[boolKey] === 'boolean') out[boolKey] = raw[boolKey]
540
543
  else problems.push(`${boolKey}: 必须是布尔值,已忽略`)
@@ -1346,7 +1349,12 @@ export function readOnlyToolFilter(ctx) {
1346
1349
  if (!Array.isArray(known)) return undefined
1347
1350
  const names = new Set(known.map(schema => schema?.name).filter(name => typeof name === 'string'))
1348
1351
  const allow = READONLY_TOOL_NAMES.filter(name => names.has(name))
1349
- return allow.length === 0 ? undefined : { allow }
1352
+ // A registry that answered but names NONE of our read-only tools cannot
1353
+ // honour the guard. Returning `undefined` here made the child inherit every
1354
+ // parent tool (fail-open) while the card only said "unsupported" — the guard
1355
+ // disappeared exactly when a host upgrade renamed things.
1356
+ if (allow.length === 0) return { blocked: 'no-readonly-guard' }
1357
+ return { allow }
1350
1358
  }
1351
1359
 
1352
1360
  // ────────────────────────────────────────────────────────────────────────────
@@ -1762,6 +1770,7 @@ function createSideEngine(ctx, configOf) {
1762
1770
  ]
1763
1771
  const seen = new Set()
1764
1772
  let fallback
1773
+ let live
1765
1774
  for (const agent of listed) {
1766
1775
  const id = agentSessionId(agent)
1767
1776
  if (id === undefined || seen.has(id)) continue
@@ -1769,11 +1778,16 @@ function createSideEngine(ctx, configOf) {
1769
1778
  const isArchived = archived.has(id)
1770
1779
  candidates.push({ id, archived: isArchived })
1771
1780
  if (fallback === undefined) fallback = { agent, id, archived: isArchived }
1772
- if (!isArchived) return { agent, id, archived: false, candidates }
1773
- }
1774
- return fallback === undefined
1781
+ if (live === undefined && !isArchived) live = { agent, id, archived: false }
1782
+ }
1783
+ // A caller that NAMED a session which is not live is answered by another
1784
+ // one (the host's historical fallback — older clients depend on it), but the
1785
+ // start frame now says WHICH session answered so the card can show it
1786
+ // instead of silently publishing into an unfamiliar conversation.
1787
+ const chosen = live ?? fallback
1788
+ return chosen === undefined
1775
1789
  ? { agent: undefined, id: undefined, archived: false, candidates }
1776
- : { ...fallback, candidates }
1790
+ : { ...chosen, candidates }
1777
1791
  }
1778
1792
 
1779
1793
  /**
@@ -1967,6 +1981,20 @@ function createSideEngine(ctx, configOf) {
1967
1981
  const preset = typeof config.sideAgentPreset === 'string' ? config.sideAgentPreset.trim() : ''
1968
1982
  const presetSupported = caps?.agentPreset === true
1969
1983
  const presetInForce = config.sideTools === 'readonly' && preset !== '' && presetSupported
1984
+ // 2.1.9: the read-only guard must fail CLOSED. Without a preset there is
1985
+ // no other real narrowing channel on this host, so a guard we cannot
1986
+ // build is a refusal, not an inheritance.
1987
+ if (config.sideTools === 'readonly' && !presetInForce
1988
+ && toolFilter !== undefined && toolFilter.blocked === 'no-readonly-guard') {
1989
+ sink.send('error', {
1990
+ id,
1991
+ code: 'no-readonly-guard',
1992
+ message: '这台宿主的工具注册表里认不出任何只读工具,无法保证侧边作答只读,已拒绝本次作答',
1993
+ retryable: false,
1994
+ fallback: 'main',
1995
+ })
1996
+ return
1997
+ }
1970
1998
  // Style and the user's own extra instruction ride the SAME persona
1971
1999
  // string, so the "persona vs prompt" gate above covers all three of
1972
2000
  // them with one branch — there is no second injection point.
@@ -1979,7 +2007,7 @@ function createSideEngine(ctx, configOf) {
1979
2007
  signal: controller.signal,
1980
2008
  ...(personaSupported ? { persona: personaText } : {}),
1981
2009
  ...(presetInForce ? { agentPreset: preset } : {}),
1982
- ...(toolFilter === undefined || !toolFilterSupported ? {} : { toolFilter }),
2010
+ ...(toolFilter === undefined || toolFilter.allow === undefined || !toolFilterSupported ? {} : { toolFilter }),
1983
2011
  })
1984
2012
  runRecord.dispose = run.dispose
1985
2013
 
@@ -2005,12 +2033,20 @@ function createSideEngine(ctx, configOf) {
2005
2033
  ? 'inherit'
2006
2034
  : (presetInForce
2007
2035
  ? 'preset'
2008
- : (toolFilter === undefined
2009
- ? 'unsupported'
2010
- : (toolFilterSupported ? 'applied' : 'unsupported'))),
2036
+ : (toolFilter !== undefined && toolFilter.blocked !== undefined
2037
+ ? 'blocked'
2038
+ : (toolFilter === undefined
2039
+ ? 'unsupported'
2040
+ : (toolFilterSupported ? 'applied' : 'unsupported')))),
2011
2041
  agentPreset: presetInForce ? preset : null,
2012
2042
  persona: personaSupported ? 'section' : 'prompt',
2013
2043
  parentArchived: parent.archived === true,
2044
+ // The session that actually owns this child. A mismatch with the
2045
+ // requested session is shown on the card, never silently absorbed.
2046
+ parentSessionId: parent.id ?? null,
2047
+ requestedSessionId: typeof request.sessionId === 'string' && request.sessionId !== ''
2048
+ ? request.sessionId
2049
+ : null,
2014
2050
  })
2015
2051
 
2016
2052
  const childId = run.id
@@ -2698,6 +2734,25 @@ export function apply(ctx, patchConfig) {
2698
2734
  writeError(res, 400, 'invalid-config', problems.join(';'))
2699
2735
  return
2700
2736
  }
2737
+ // 2.1.9: changing WHERE mail is sent (host/port/tls/user) invalidates the
2738
+ // stored authorization code. Keeping it made "host:" a one-field takeover:
2739
+ // point the host at an attacker and the next /feedback hands them the
2740
+ // password in cleartext AUTH PLAIN. Losing the password is recoverable;
2741
+ // leaking it is not.
2742
+ const endpointChanged = ['feedbackSmtpHost', 'feedbackSmtpPort', 'feedbackSmtpSecure', 'feedbackSmtpUser']
2743
+ .some(key => accepted[key] !== undefined && String(accepted[key]) !== String(state.config[key] ?? ''))
2744
+ if (endpointChanged && accepted.feedbackSmtpPass === undefined) {
2745
+ delete userLayer.feedbackSmtpPass
2746
+ problems.push('SMTP 目标已变更,已保存的授权码随之作废,请重新填写')
2747
+ }
2748
+ // The side answerer inheriting the whole tool set is the one irreversible
2749
+ // kind of change here (a page-level script can ask the host to run with the
2750
+ // session's tools). A written confirmation does not stop same-origin code,
2751
+ // but it stops the accident and makes the intent auditable.
2752
+ if (accepted.sideTools === 'inherit' && body.confirmInheritTools !== true) {
2753
+ writeError(res, 400, 'privileged-key', 'sideTools=inherit 需要显式确认(confirmInheritTools: true)')
2754
+ return
2755
+ }
2701
2756
  userLayer = { ...userLayer, ...accepted }
2702
2757
  const saved = writePersistedConfig(userLayer, ctx.logger)
2703
2758
  if (!saved.ok) {
package/locale/en.json CHANGED
@@ -41,5 +41,7 @@
41
41
  "versionViewing": "Viewing an older version (read-only)",
42
42
  "suggestionsLabel": "Suggested follow-ups",
43
43
  "setFollowUps": "Suggest follow-ups at the end",
44
- "setFollowUpsHint": "The model ends each answer with a <followups> block of 2–3 next questions, rendered as clickable chips (no extra request)"
44
+ "setFollowUpsHint": "The model ends each answer with a <followups> block of 2–3 next questions, rendered as clickable chips (no extra request)",
45
+ "setAutoExpandOnDone": "Expand the card when the answer finishes",
46
+ "setAutoExpandOnDoneHint": "On: the answer appears in the card the moment it settles. Off: the one-line capsule stays until you click it"
45
47
  }
package/locale/zh.json CHANGED
@@ -41,5 +41,7 @@
41
41
  "versionViewing": "正在查看历史版本(只读)",
42
42
  "suggestionsLabel": "建议追问",
43
43
  "setFollowUps": "答案末尾给出建议追问",
44
- "setFollowUpsHint": "开启后模型会用 <followups> 块在答案末尾给出 2–3 个可点的下一步问题,点一下即追问(不额外发请求)"
44
+ "setFollowUpsHint": "开启后模型会用 <followups> 块在答案末尾给出 2–3 个可点的下一步问题,点一下即追问(不额外发请求)",
45
+ "setAutoExpandOnDone": "答完自动展开卡片",
46
+ "setAutoExpandOnDoneHint": "开启后,回答一结束就展开成卡片显示正文;关闭后保持一行胶囊,点一下才展开"
45
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidecard-ask",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "description": "DSH 划词追问(侧边卡片作答):在聊天区与任务区选中文本就地追问,答案由独立子代理在侧边卡片里流式呈现,也可选择落回主对话。",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -21,7 +21,8 @@
21
21
  "icon.svg",
22
22
  "README.md",
23
23
  "CHANGELOG.md",
24
- "LICENSE"
24
+ "LICENSE",
25
+ "NODE-HOST-CONTRACT.md"
25
26
  ],
26
27
  "engines": {
27
28
  "node": ">=20",