@zmainer/dsh-wx-bridge 1.0.12 → 1.0.13
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.md +8 -0
- package/lib/kernel/bridge.mjs +69 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,14 @@ dsh plugin --profile <profile> add @zmainer/dsh-wx-bridge
|
|
|
147
147
|
|
|
148
148
|
## 最近变更
|
|
149
149
|
|
|
150
|
+
- **1.0.13**:修「`/model` 回『模型目录暂时不可用』」(用户反馈)——
|
|
151
|
+
模型/推理强度目录是**纯内存**的,且只在 `session/new` 分支填充;而 peer 一旦有会话(正常使用后的必然状态)
|
|
152
|
+
就走 `session/resume` 或"本进程已挂载"分支,**这两个分支把上游返回的 `configOptions` 丢掉了**,
|
|
153
|
+
于是 `/model` 永远读到空目录。现在:① `resume` 的返回接住;② `set_config_option` 的返回也带完整目录,
|
|
154
|
+
`applyPeerConfig` 顺手吸收(覆盖"已挂载"分支);③ 目录**落盘 `state.json`** 并在启动时回读
|
|
155
|
+
(桥重启后立刻可用);④ 空目录文案改成可操作的(并提示 `/new` 或直接 `provider/model`);
|
|
156
|
+
⑤ `/status` 显示目录条数,新增 `--selftest-catalog [sessionId]` 一次看清来源。
|
|
157
|
+
上游三处返回同构(`@deepseek-ai/dsh-acp`:`session/new` / `session/resume` / `session/set_config_option`)。
|
|
150
158
|
- **1.0.12**:**支持图片识别**(用户要求)——
|
|
151
159
|
① 微信里的图片会被下载并解密(协议:`image_item.media.encrypt_query_param/full_url` + `aes_key`,
|
|
152
160
|
走 `https://novac2c.cdn.weixin.qq.com/c2c/download`,**AES-128-ECB** 解密),落到 `<数据目录>/media/`;
|
package/lib/kernel/bridge.mjs
CHANGED
|
@@ -718,22 +718,50 @@ function overlayFor(key) {
|
|
|
718
718
|
return overlayCache.get(key) || ACP_PATCH
|
|
719
719
|
}
|
|
720
720
|
function peerMode(peer) { return presetKey(peer) }
|
|
721
|
-
/**
|
|
721
|
+
/**
|
|
722
|
+
* ACP 会话配置目录(供 /model、/effort 列选用)。
|
|
723
|
+
* 上游在这三处都会返回 `configOptions`:`session/new`、**`session/resume`**、`session/set_config_option`
|
|
724
|
+
*(@deepseek-ai/dsh-acp 的 lib/index.js 同构返回)。**三个入口都要接住** ——
|
|
725
|
+
* 原来只在 new 分支填充,而 peer 一旦有会话(正常使用后的必然状态)就走 resume,
|
|
726
|
+
* 于是 /model 永远读到空目录(2026-09-22 用户反馈)。
|
|
727
|
+
*/
|
|
722
728
|
let acpModelCatalog = []
|
|
723
729
|
let acpEffortCatalog = []
|
|
724
730
|
function flattenConfigOptions(opts) {
|
|
731
|
+
let filled = 0
|
|
725
732
|
for (const o of opts || []) {
|
|
726
733
|
if (o.id === 'model') {
|
|
727
734
|
const list = []
|
|
728
735
|
for (const g of o.options || []) for (const it of g.options || []) list.push({ group: g.name || g.group || '', label: it.name || it.value, value: it.value })
|
|
729
|
-
if (list.length) acpModelCatalog = list
|
|
736
|
+
if (list.length) { acpModelCatalog = list; filled++ }
|
|
730
737
|
}
|
|
731
738
|
if (o.id === 'reasoning_effort') {
|
|
732
739
|
const list = []
|
|
733
740
|
for (const it of o.options || []) list.push({ label: it.name || it.value, value: it.value })
|
|
734
|
-
if (list.length) acpEffortCatalog = list
|
|
741
|
+
if (list.length) { acpEffortCatalog = list; filled++ }
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return filled
|
|
745
|
+
}
|
|
746
|
+
/** 目录落盘:桥重启后 /model 立刻可用(原来纯内存,重启即空)。 */
|
|
747
|
+
function persistCatalog(source) {
|
|
748
|
+
try {
|
|
749
|
+
if (!acpModelCatalog.length && !acpEffortCatalog.length) return
|
|
750
|
+
state.acpCatalog = {
|
|
751
|
+
models: acpModelCatalog.slice(0, 200), efforts: acpEffortCatalog.slice(0, 50),
|
|
752
|
+
source, at: new Date().toISOString(),
|
|
735
753
|
}
|
|
754
|
+
saveState(state)
|
|
755
|
+
} catch (e) { log('catalog-persist-failed', { error: String(e?.message ?? e) }) }
|
|
756
|
+
}
|
|
757
|
+
/** 统一入口:拿到 configOptions 就 flatten + 落盘 + 记日志(三个来源共用)。 */
|
|
758
|
+
function absorbConfigOptions(opts, source) {
|
|
759
|
+
const n = flattenConfigOptions(opts)
|
|
760
|
+
if (n) {
|
|
761
|
+
log('catalog-filled', { source, models: acpModelCatalog.length, efforts: acpEffortCatalog.length })
|
|
762
|
+
persistCatalog(source)
|
|
736
763
|
}
|
|
764
|
+
return n
|
|
737
765
|
}
|
|
738
766
|
function getAcpHost(key) {
|
|
739
767
|
const k = String(key || ACP_DEFAULT_PRESET).trim() || ACP_DEFAULT_PRESET
|
|
@@ -881,7 +909,9 @@ async function ensurePeerSession(peer) {
|
|
|
881
909
|
return slot.id
|
|
882
910
|
}
|
|
883
911
|
try {
|
|
884
|
-
await host.resumeSession(slot.id, cwd)
|
|
912
|
+
const r = await host.resumeSession(slot.id, cwd)
|
|
913
|
+
// resume 也会带回配置目录——原来这里把它丢了,导致 /model 在"有会话"时永远空目录
|
|
914
|
+
absorbConfigOptions(r?.configOptions, 'resume')
|
|
885
915
|
await applyPeerConfig(peer, slot.id)
|
|
886
916
|
return slot.id
|
|
887
917
|
} catch (e) {
|
|
@@ -895,7 +925,7 @@ async function ensurePeerSession(peer) {
|
|
|
895
925
|
slot.cwd = cwd
|
|
896
926
|
// 卖点落地:桥写出的会话要立刻出现在桌面 GUI 的工作区列表里(不靠人手动 /attach)。
|
|
897
927
|
void reportSessionToHost(slot.id, cwd)
|
|
898
|
-
|
|
928
|
+
absorbConfigOptions(r?.configOptions, 'new')
|
|
899
929
|
if (Array.isArray(r?.configOptions)) peer.acpConfig = r.configOptions.map((o) => o.id).join(',')
|
|
900
930
|
await applyPeerConfig(peer, slot.id)
|
|
901
931
|
await saveState(state)
|
|
@@ -998,8 +1028,10 @@ async function applyPeerConfig(peer, sessionId) {
|
|
|
998
1028
|
}
|
|
999
1029
|
const host = getAcpHost(peerMode(peer))
|
|
1000
1030
|
try {
|
|
1001
|
-
|
|
1002
|
-
|
|
1031
|
+
// set_config_option 的返回里同样带完整 configOptions → 顺手补目录
|
|
1032
|
+
//("本进程已挂载"这条分支就是靠它覆盖的:那里没有 new/resume 的返回可接)
|
|
1033
|
+
if (peer.acpModel) absorbConfigOptions((await host.setConfigOption(sid, 'model', peer.acpModel))?.configOptions, 'set-model')
|
|
1034
|
+
if (peer.acpEffort) absorbConfigOptions((await host.setConfigOption(sid, 'reasoning_effort', peer.acpEffort))?.configOptions, 'set-effort')
|
|
1003
1035
|
} catch (e) { log('acp-config-apply-failed', { error: String(e?.message ?? e) }) }
|
|
1004
1036
|
}
|
|
1005
1037
|
|
|
@@ -1149,6 +1181,12 @@ function enqueueTask(token, peerKey, from, text, contextToken) {
|
|
|
1149
1181
|
|
|
1150
1182
|
let state = loadState()
|
|
1151
1183
|
let lastSelfSave = 0
|
|
1184
|
+
// 目录落盘的回读:桥刚起来、还没跟 ACP 交换过配置时,/model、/effort 也该有清单。
|
|
1185
|
+
if (Array.isArray(state.acpCatalog?.models) && state.acpCatalog.models.length) {
|
|
1186
|
+
acpModelCatalog = state.acpCatalog.models
|
|
1187
|
+
acpEffortCatalog = Array.isArray(state.acpCatalog.efforts) ? state.acpCatalog.efforts : []
|
|
1188
|
+
log('catalog-from-state', { models: acpModelCatalog.length, efforts: acpEffortCatalog.length, at: state.acpCatalog.at || '' })
|
|
1189
|
+
}
|
|
1152
1190
|
/**
|
|
1153
1191
|
* 外部(宿主半的扫码登记、面板动作)会改同一份 state.json;桥只在启动时读一次的话,
|
|
1154
1192
|
* 「扫码登记了新主人」要等到下次重启才生效 —— 这里在每轮轮询时比较 mtime,
|
|
@@ -1293,7 +1331,8 @@ async function handleMessage(token, msg) {
|
|
|
1293
1331
|
'预设档=' + presetKey(peer) + '|默认档=' + ACP_DEFAULT_PRESET,
|
|
1294
1332
|
'原生会话=' + (sessionSlot(peer, peerMode(peer)).id ? String(sessionSlot(peer, peerMode(peer)).id).slice(0, 12) + '…' : '(未建立)')
|
|
1295
1333
|
+ '|acp pid=' + (getAcpHost(peerMode(peer)).pid || 0),
|
|
1296
|
-
'模型=' + (peer.acpModel || '(profile 默认)') + '|推理强度=' + (peer.acpEffort || '(默认)')
|
|
1334
|
+
'模型=' + (peer.acpModel || '(profile 默认)') + '|推理强度=' + (peer.acpEffort || '(默认)'),
|
|
1335
|
+
'模型目录=' + (acpModelCatalog.length || '(未拿到)') + ' 项|推理强度目录=' + (acpEffortCatalog.length || '(未拿到)') + ' 项'].join(NL), ctx)
|
|
1297
1336
|
if (cmd === '/task') {
|
|
1298
1337
|
const list = Object.values(state.tasks).slice(-8)
|
|
1299
1338
|
if (!list.length) return reply(token, from, '当前无任务。发任意文本即可派活。', ctx)
|
|
@@ -1388,7 +1427,8 @@ async function handleMessage(token, msg) {
|
|
|
1388
1427
|
try { await ensurePeerSession(peer) } catch (e) { return reply(token, from, 'ACP 会话不可用:' + String(e?.message ?? e), ctx) }
|
|
1389
1428
|
const pickArg = body.trim().slice(6).trim()
|
|
1390
1429
|
if (!pickArg) {
|
|
1391
|
-
if (!acpModelCatalog.length) return reply(token, from, '
|
|
1430
|
+
if (!acpModelCatalog.length) return reply(token, from, '模型目录还没拿到(本进程还没跟 ACP 会话交换过配置)。'
|
|
1431
|
+
+ NL + '试:发任意一条消息、或 /new 后再发 /model;也可以直接 /model provider/model 指定。', ctx)
|
|
1392
1432
|
const cur = peer.acpModel || ''
|
|
1393
1433
|
const label = (m) => { try { const [pv, md] = JSON.parse(m.value); return pv + '/' + md } catch { return m.value } }
|
|
1394
1434
|
const rows = acpModelCatalog.map((m, i) => (i + 1) + '. ' + m.label + '(' + m.group + '|' + label(m) + ')' + (m.value === cur ? ' ← 当前' : ''))
|
|
@@ -1485,6 +1525,24 @@ async function main() {
|
|
|
1485
1525
|
}, null, 1))
|
|
1486
1526
|
process.exit(0)
|
|
1487
1527
|
}
|
|
1528
|
+
if (args.includes('--selftest-catalog')) {
|
|
1529
|
+
const sid = argOf('--selftest-catalog', '')
|
|
1530
|
+
const host = getAcpHost()
|
|
1531
|
+
let src = 'none', err = null
|
|
1532
|
+
try {
|
|
1533
|
+
if (sid) { const r = await host.resumeSession(sid, CWD); src = 'resume:' + absorbConfigOptions(r?.configOptions, 'selftest-resume') }
|
|
1534
|
+
else { const r = await host.newSession(CWD); src = 'new:' + absorbConfigOptions(r?.configOptions, 'selftest-new') }
|
|
1535
|
+
} catch (e) { err = String(e?.message ?? e) }
|
|
1536
|
+
host.stop()
|
|
1537
|
+
await saveState(state) // 等落盘完成再退出(saveState 是异步链,不然文件还没写就 process.exit)
|
|
1538
|
+
console.log('[selftest-catalog] ' + JSON.stringify({
|
|
1539
|
+
session: sid || '(new)', source: src, error: err,
|
|
1540
|
+
models: acpModelCatalog.length, sample: acpModelCatalog.slice(0, 3).map((m) => m.label),
|
|
1541
|
+
efforts: acpEffortCatalog.map((e) => e.value),
|
|
1542
|
+
persisted: !!(state.acpCatalog && state.acpCatalog.models && state.acpCatalog.models.length),
|
|
1543
|
+
}, null, 1))
|
|
1544
|
+
process.exit(acpModelCatalog.length ? 0 : 1)
|
|
1545
|
+
}
|
|
1488
1546
|
if (args.includes('--selftest-media')) {
|
|
1489
1547
|
const f = argOf('--selftest-media', '')
|
|
1490
1548
|
let parsed = {}
|
|
@@ -1558,7 +1616,8 @@ async function main() {
|
|
|
1558
1616
|
for (const [, h] of acpHosts) h.stop()
|
|
1559
1617
|
console.log('[selftest-acp] ' + JSON.stringify({
|
|
1560
1618
|
bin: resolveAcpBin(), binOk: fileOk(resolveAcpBin()), home: ACP_HOME, preset: presetKey(peer), patch: overlayFor(presetKey(peer)),
|
|
1561
|
-
session: sessionSlot(peer, peer
|
|
1619
|
+
session: sessionSlot(peer, presetKey(peer)).id,
|
|
1620
|
+
catalog: acpModelCatalog.length,
|
|
1562
1621
|
turn1: r1?.text, turn2: r2?.text, turn3: r3?.text, notice: r2?.notice || null,
|
|
1563
1622
|
}))
|
|
1564
1623
|
process.exit(0)
|