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/CHANGELOG.md +86 -0
- package/NODE-HOST-CONTRACT.md +392 -0
- package/README.md +67 -0
- package/client.js +308 -38
- package/index.js +66 -11
- package/locale/en.json +3 -1
- package/locale/zh.json +3 -1
- package/package.json +3 -2
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.
|
|
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
|
-
|
|
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)
|
|
1773
|
-
}
|
|
1774
|
-
|
|
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
|
-
: { ...
|
|
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
|
|
2009
|
-
? '
|
|
2010
|
-
: (
|
|
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.
|
|
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",
|