dsh-second-review 1.3.4 → 1.3.6
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 +13 -7
- package/client.js +438 -13
- package/index.js +221 -9
- package/package.json +1 -1
- package/test/client.smoke.mjs +32 -0
- package/test/core.test.mjs +118 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Independent second-opinion review for DeepSeek Harness.
|
|
4
4
|
> 主模型负责执行,副模型负责复核;结论以卡片形式附在原回复下方。
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
@@ -60,7 +60,8 @@ SEC 是一个**主执行者 + 独立监督者**的组合:主模型完成回答
|
|
|
60
60
|
| 功能 | 作用 | 费用 |
|
|
61
61
|
|-|-|-|
|
|
62
62
|
| **二次评审** | 复核当前轮(含此前对话) | 一次副模型调用 |
|
|
63
|
-
| **自动评审** |
|
|
63
|
+
| **自动评审** | 新回复到达后自动复核一次(**默认关闭**);范围可选「每条」或「只有动过手」 | 同上,按条计 |
|
|
64
|
+
| **用量** | 卡片末尾写出本轮主模型与本次复核的 token | 无(从日志算,不调模型) |
|
|
64
65
|
| **副模型主刀** | 交换角色:下一轮由副模型回答,原主模型复核 | 按对应模型计 |
|
|
65
66
|
| **让主模型读** | 将评审结论交给主模型,促其正面回应 | 一次主模型调用 |
|
|
66
67
|
| **停止** | 中止进行中的复核;已生成内容保留可查 | 已产生费用不退回 |
|
|
@@ -129,7 +130,7 @@ SEC 是一个**主执行者 + 独立监督者**的组合:主模型完成回答
|
|
|
129
130
|
| 反复挑标点、路径、措辞 | 多为模型话多,换一个即可,不必修改需求 |
|
|
130
131
|
| 长对话中持续变慢 | 部分模型会逐句过一遍,属模型行为,不是插件故障 |
|
|
131
132
|
|
|
132
|
-
> 模型名称为写作时的示例。实际可选范围以你 DSH 中已配置的 provider
|
|
133
|
+
> 模型名称为写作时的示例。实际可选范围以你 DSH 中已配置的 provider 为准;不指定时默认走 **Pro 档**(自动跟随,见 07 `modelAuto`)。
|
|
133
134
|
|
|
134
135
|
---
|
|
135
136
|
|
|
@@ -173,11 +174,11 @@ file:C:\Users\<用户名>\Documents\dsh-second-review
|
|
|
173
174
|
|
|
174
175
|
**方式三:压缩包**
|
|
175
176
|
|
|
176
|
-
将 `dsh-second-review-1.3.
|
|
177
|
+
将 `dsh-second-review-1.3.6.tgz` 发给对方,在「添加插件」中选择该压缩包。
|
|
177
178
|
|
|
178
179
|
**设置页**位于 **插件 → 二次评审**,分四段:01 模型 / 02 评审模式 / 03 权限 / 04 关于。修改后需点击底部「保存」,未保存时页面会提示。
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
**首次使用**:建议自动评审保持关闭,先点「先看效果(不花钱)」,再做第一次真实复核(首次会展示费用与数据去向说明)。副模型默认就是 **Pro 档**、跟着版本走;主模型如果也是它,点评审时会先弹一个窗让你换——同一双眼睛看不见自己。
|
|
181
182
|
|
|
182
183
|
**更新**:以新版本覆盖同一目录,再重新添加一次相同路径,然后重启 DSH。配置与评审记录保存在插件目录之外,不会因覆盖丢失。
|
|
183
184
|
|
|
@@ -189,8 +190,11 @@ file:C:\Users\<用户名>\Documents\dsh-second-review
|
|
|
189
190
|
|
|
190
191
|
| 字段 | 默认 | 说明 |
|
|
191
192
|
|-|-|-|
|
|
192
|
-
| `provider` / `model` | `deepseek-account` / `deepseek-
|
|
193
|
+
| `provider` / `model` | `deepseek-account` / `deepseek-v4-pro` | 副模型(`modelAuto` 打开时以自动挑到的 Pro 档为准) |
|
|
194
|
+
| `modelAuto` | `true` | 自动挑这个 provider 里名字带 `pro` 的最新一版;关掉就用 `model` 里那个具体 id |
|
|
195
|
+
| `hideSameSourceModal` | `false` | 主副重合提示窗的「不再提示」 |
|
|
193
196
|
| `auto` | `false` | 新回复自动复核 |
|
|
197
|
+
| `autoMode` | `every` | 自动评审范围:`every` 每条都评/`actions` 只有动过手(写文件、跑命令、发消息、工作区有改动)才评 |
|
|
194
198
|
| `mock` | `false` | 测试模式:模拟数据,不调模型、不落盘 |
|
|
195
199
|
| `allowWrite` | `false` | 是否向副模型提供写文件工具 |
|
|
196
200
|
| `budgetSec` | `0` | 总时长上限(秒),`0` 为不限 |
|
|
@@ -285,7 +289,7 @@ file:C:\Users\<用户名>\Documents\dsh-second-review
|
|
|
285
289
|
|
|
286
290
|
## 12 · 版本
|
|
287
291
|
|
|
288
|
-
当前 **1.3.
|
|
292
|
+
当前 **1.3.6**。
|
|
289
293
|
|
|
290
294
|
| 版本 | 主要内容 |
|
|
291
295
|
|-|-|
|
|
@@ -305,6 +309,8 @@ file:C:\Users\<用户名>\Documents\dsh-second-review
|
|
|
305
309
|
| **1.3.2** | 上架 npm;补仓库与 Issue 入口;README 增补 30 秒速览、设计动机、卡片示例图、故障排查、反馈渠道 |
|
|
306
310
|
| **1.3.3** | 仓库根目录化后同步:repository 去掉 directory,README 图片改回相对路径(由 npm 依仓库地址解析) |
|
|
307
311
|
| **1.3.4** | 安装文档补「按包名安装」;修正压缩包版本号与更新日志里的错字 |
|
|
312
|
+
| **1.3.5** | 副模型与主模型是同一个模型时,卡片标出「与主模型相同 · 自评」;设置页补默认值提醒;修正「自动评审默认可关闭」这句歧义 |
|
|
313
|
+
| **1.3.6** | 副模型默认改走 **Pro 档**、且 `modelAuto` 跟着 DSH 的模型清单自动换代;点「二次评审」前若主副重合,弹一个明暗跟随皮肤的黑白窗,可直接换模型(可勾「不再提示」);评审中随机轮播九句碎碎念;卡片末尾显示用量(本轮主模型+本次复核,全从日志和回执算,不额外调模型);自动评审加「范围」开关:每条都评/只有动过手才评,跳过的轮次卡片上写明;设置页加三个预设(只改开关,不换模型) |
|
|
308
314
|
|
|
309
315
|
---
|
|
310
316
|
|
package/client.js
CHANGED
|
@@ -31,6 +31,21 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
用 em 而不是 px,他自己调了系统/界面字号,卡片跟着一起变。 */
|
|
32
32
|
const FS = { body: '1em', small: '0.95em', min: '0.9em' }
|
|
33
33
|
|
|
34
|
+
/* ---------------- 思考中的碎碎念(1.3.6) ----------------
|
|
35
|
+
↓↓↓ 文案填这里:一行一句,随便加、随便删,随机轮着放 ↓↓↓
|
|
36
|
+
他 2026-11-11 亲手写的九句,就按这个顺序存,每次随机挑一句、不连着重复。 */
|
|
37
|
+
const THINK_LINES = [
|
|
38
|
+
'嗯……帮你理一理',
|
|
39
|
+
'正在认真评审,稍等哦',
|
|
40
|
+
'让我沉下来想一想',
|
|
41
|
+
'试着从这边看看',
|
|
42
|
+
'搬个小板凳坐下来想',
|
|
43
|
+
'嗯……这个说法稳妥吗',
|
|
44
|
+
'让我多问自己一个为什么',
|
|
45
|
+
'补一块砖,站得更稳',
|
|
46
|
+
'调调焦距,看得更清',
|
|
47
|
+
]
|
|
48
|
+
|
|
34
49
|
/* ---------------- 玻璃(方案 B:搬「开关」,不搬代码) ----------------
|
|
35
50
|
壁纸插件 dsh-plugin-wallpaper-engine 把玻璃参数写成 <body> 上的 CSS 变量,
|
|
36
51
|
body 下面所有元素都能读到。我们只读这几个:
|
|
@@ -46,6 +61,21 @@ window.__ModuleLoader__.load({
|
|
|
46
61
|
|
|
47
62
|
const R = 10 // "角圆润的方形"(他 2026-11-09 说的)—— 不是胶囊,也不是直角
|
|
48
63
|
const ACCENT = 'var(--we-accent, #3b82f6)'
|
|
64
|
+
|
|
65
|
+
/* 1.3.6:弹窗要"非黑即白" —— 跟着现在的皮肤判个明暗,不猜也不写死深色 */
|
|
66
|
+
const isDarkUI = () => {
|
|
67
|
+
try {
|
|
68
|
+
const bg = getComputedStyle(document.body).backgroundColor || ''
|
|
69
|
+
const m = bg.match(/\d+(\.\d+)?/g)
|
|
70
|
+
if (!m || m.length < 3) return true
|
|
71
|
+
const r = Number(m[0])
|
|
72
|
+
const g = Number(m[1])
|
|
73
|
+
const b = Number(m[2])
|
|
74
|
+
return 0.299 * r + 0.587 * g + 0.114 * b < 128
|
|
75
|
+
} catch {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
49
79
|
const HAIRLINE = 'rgba(128,128,128,0.24)'
|
|
50
80
|
const HAIRLINE_SOFT = 'rgba(128,128,128,0.16)'
|
|
51
81
|
const INK_SOFT = 'rgba(128,128,128,0.18)'
|
|
@@ -414,7 +444,7 @@ window.__ModuleLoader__.load({
|
|
|
414
444
|
|
|
415
445
|
/* ---------------- 设置(模块级,整页共用一份) ---------------- */
|
|
416
446
|
|
|
417
|
-
const settings = { value: null, pending: null }
|
|
447
|
+
const settings = { value: null, pending: null, resolved: '' }
|
|
418
448
|
|
|
419
449
|
function loadSettings(force) {
|
|
420
450
|
if (!force && settings.value) return Promise.resolve(settings.value)
|
|
@@ -423,6 +453,8 @@ window.__ModuleLoader__.load({
|
|
|
423
453
|
.then((r) => r.json())
|
|
424
454
|
.then((j) => {
|
|
425
455
|
settings.value = (j && j.ok && j.config) || { auto: false }
|
|
456
|
+
// 1.3.6:自动 Pro 档时,后端会告诉我们最终落到哪个模型(卡片和弹窗都要显示它)
|
|
457
|
+
settings.resolved = (j && j.ok && j.resolvedModel) || ''
|
|
426
458
|
return settings.value
|
|
427
459
|
})
|
|
428
460
|
.catch(() => {
|
|
@@ -438,7 +470,7 @@ window.__ModuleLoader__.load({
|
|
|
438
470
|
|
|
439
471
|
/* ---------------- 副模型主刀(每个会话各存各的,互不覆盖) ---------------- */
|
|
440
472
|
|
|
441
|
-
const swap = { bySession: {}, pending: {} }
|
|
473
|
+
const swap = { bySession: {}, pending: {}, main: {} }
|
|
442
474
|
|
|
443
475
|
/* 同一会话里所有卡片共用一份"主刀开没开",不然 A 卡开了 B 卡还显示关 */
|
|
444
476
|
const swapShared = { value: {}, listeners: new Set() }
|
|
@@ -469,7 +501,11 @@ window.__ModuleLoader__.load({
|
|
|
469
501
|
swap.pending[k] = fetch(API + '/swap?sessionId=' + encodeURIComponent(k))
|
|
470
502
|
.then((r) => r.json())
|
|
471
503
|
.then((j) => {
|
|
472
|
-
if (j && j.ok)
|
|
504
|
+
if (j && j.ok) {
|
|
505
|
+
swap.bySession[k] = !!j.on
|
|
506
|
+
// 1.3.6:顺手把"这个会话现在的主模型"留住 —— 点评审前要拿它比是否重合
|
|
507
|
+
if (j.current && typeof j.current === 'object') swap.main[k] = { provider: String(j.current.provider || ''), model: String(j.current.model || '') }
|
|
508
|
+
}
|
|
473
509
|
return !!swap.bySession[k]
|
|
474
510
|
})
|
|
475
511
|
.catch(() => !!swap.bySession[k])
|
|
@@ -698,6 +734,22 @@ window.__ModuleLoader__.load({
|
|
|
698
734
|
// 在一张卡上开了,别的卡上的开关还显示"关"(GPT 4.6 第 4 条)。改成共用一份。
|
|
699
735
|
const [swapOn, setSwapOn] = useSwap(sidKey)
|
|
700
736
|
|
|
737
|
+
// 碎碎念:正在评的时候才轮播;停了/到点了就不逗他(他 2026-11-11 要的"轻松一点")
|
|
738
|
+
const [quipIdx, setQuipIdx] = useState(0)
|
|
739
|
+
const quipOn = !!(entry && entry.state === 'loading')
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
if (!quipOn) return undefined
|
|
742
|
+
const t = setInterval(() => {
|
|
743
|
+
setQuipIdx((i) => {
|
|
744
|
+
if (THINK_LINES.length < 2) return 0
|
|
745
|
+
let n = i
|
|
746
|
+
while (n === i) n = Math.floor(Math.random() * THINK_LINES.length)
|
|
747
|
+
return n
|
|
748
|
+
})
|
|
749
|
+
}, 3200)
|
|
750
|
+
return () => clearInterval(t)
|
|
751
|
+
}, [quipOn])
|
|
752
|
+
|
|
701
753
|
useEffect(() => {
|
|
702
754
|
let alive = true
|
|
703
755
|
loadSwap(sessionId).then((v) => {
|
|
@@ -740,6 +792,33 @@ window.__ModuleLoader__.load({
|
|
|
740
792
|
},
|
|
741
793
|
'副模型主刀' + (swapOn ? ':开' : ''),
|
|
742
794
|
)
|
|
795
|
+
/* 1.3.6:这张卡这次花了多少 —— 全从日志和返回里算,不为显示数字多调一次模型。
|
|
796
|
+
复核=副模型那次(要它真给了用量才显示);本轮=主模型这一轮的账(日志里本来就有)。 */
|
|
797
|
+
const fmtTok = (n) => {
|
|
798
|
+
const v = Number(n) || 0
|
|
799
|
+
if (!v) return '0'
|
|
800
|
+
return v >= 1000 ? (v / 1000).toFixed(v >= 10000 ? 0 : 1).replace(/\.0$/, '') + 'k' : String(v)
|
|
801
|
+
}
|
|
802
|
+
const usageChip = (u) => {
|
|
803
|
+
if (!u || typeof u !== 'object') return null
|
|
804
|
+
const r = u.review
|
|
805
|
+
const t = u.turn
|
|
806
|
+
const parts = []
|
|
807
|
+
if (r && (r.input || r.output)) parts.push('复核 ' + fmtTok(r.input) + '进/' + fmtTok(r.output) + '出')
|
|
808
|
+
if (t && (t.input || t.output)) parts.push('本轮 ' + fmtTok(t.input) + '进/' + fmtTok(t.output) + '出')
|
|
809
|
+
if (!parts.length) return null
|
|
810
|
+
const cache = []
|
|
811
|
+
if (r && r.cacheRead) cache.push('复核缓存读 ' + fmtTok(r.cacheRead))
|
|
812
|
+
if (t && t.cacheRead) cache.push('本轮缓存读 ' + fmtTok(t.cacheRead))
|
|
813
|
+
return React.createElement(
|
|
814
|
+
'span',
|
|
815
|
+
{
|
|
816
|
+
style: { opacity: 0.5, fontSize: FS.min, whiteSpace: 'nowrap' },
|
|
817
|
+
title: '这些数字是从会话日志和调用回执里算的,没有为显示它们多调一次模型' + (cache.length ? ';' + cache.join(',') : ''),
|
|
818
|
+
},
|
|
819
|
+
parts.join(' · '),
|
|
820
|
+
)
|
|
821
|
+
}
|
|
743
822
|
const state = entry && entry.state
|
|
744
823
|
const major = (entry && entry.major) || []
|
|
745
824
|
const minor = (entry && entry.minor) || []
|
|
@@ -752,7 +831,7 @@ window.__ModuleLoader__.load({
|
|
|
752
831
|
let alive = true
|
|
753
832
|
loadSettings().then((v) => {
|
|
754
833
|
if (alive) setAuto(!!(v && v.auto))
|
|
755
|
-
if (alive) setReviewerName(v && v.model ? String(v.model) : '')
|
|
834
|
+
if (alive) setReviewerName(v && (v.resolved || v.model) ? String(v.resolved || v.model) : '')
|
|
756
835
|
})
|
|
757
836
|
return () => {
|
|
758
837
|
alive = false
|
|
@@ -794,11 +873,22 @@ window.__ModuleLoader__.load({
|
|
|
794
873
|
fetch(url, {
|
|
795
874
|
method: 'POST',
|
|
796
875
|
headers: { 'content-type': 'application/json' },
|
|
797
|
-
body: JSON.stringify({ sessionId: sessionId || '', seq: seq, job, mock: kind === 'mock' }),
|
|
876
|
+
body: JSON.stringify({ sessionId: sessionId || '', seq: seq, job, mock: kind === 'mock', auto: kind === 'auto' }),
|
|
798
877
|
})
|
|
799
878
|
.then((r) => r.json())
|
|
800
879
|
.then((json) => {
|
|
801
880
|
if (json && json.ok) {
|
|
881
|
+
// 1.3.6:闸门跳过 —— 没花钱,也别装成评过了
|
|
882
|
+
if (json.skipped) {
|
|
883
|
+
setEntry({
|
|
884
|
+
state: 'skipped',
|
|
885
|
+
major: [],
|
|
886
|
+
minor: [],
|
|
887
|
+
note: String(json.reason || '自动评审跳过了这一轮'),
|
|
888
|
+
auto: kind === 'auto',
|
|
889
|
+
})
|
|
890
|
+
return
|
|
891
|
+
}
|
|
802
892
|
if (json.verdict === 'error') {
|
|
803
893
|
setEntry({
|
|
804
894
|
state: 'error',
|
|
@@ -834,6 +924,8 @@ window.__ModuleLoader__.load({
|
|
|
834
924
|
timedOut: !!json.timedOut,
|
|
835
925
|
mock: !!json.mock,
|
|
836
926
|
steps: Array.isArray(json.steps) ? json.steps : [],
|
|
927
|
+
// 1.3.6:这一次的账(主模型那轮 + 复核自己),拿不到就是 null
|
|
928
|
+
usage: json.usage || null,
|
|
837
929
|
})
|
|
838
930
|
} else {
|
|
839
931
|
setEntry({ state: 'error', major: [], minor: [], note: String((json && json.error) || '未知原因') })
|
|
@@ -911,10 +1003,233 @@ window.__ModuleLoader__.load({
|
|
|
911
1003
|
// 手动再评:允许一次。上次是假数据就继续用假数据(别让人点一下"重评"就悄悄花了钱)
|
|
912
1004
|
const rerun = () => {
|
|
913
1005
|
inFlight.add(key)
|
|
914
|
-
|
|
1006
|
+
startReview(entry && entry.mock ? 'mock' : 'manual')
|
|
915
1007
|
inFlight.delete(key)
|
|
916
1008
|
}
|
|
917
1009
|
|
|
1010
|
+
/* ---------------- 1.3.6:主副重合 → 点之前拦一下 ----------------
|
|
1011
|
+
重合的判断:这个会话的当前主模型(/swap 顺手带回来的)和副模型是同一个。
|
|
1012
|
+
只拦"用户自己点评审"这一刻;自动评审照旧在卡片上标出来,不弹窗打扰。 */
|
|
1013
|
+
const [fix, setFix] = useState(null)
|
|
1014
|
+
const [fixNoMore, setFixNoMore] = useState(false)
|
|
1015
|
+
const [fixProviders, setFixProviders] = useState([])
|
|
1016
|
+
const [fixModels, setFixModels] = useState([])
|
|
1017
|
+
const fixProvider = fix ? String(fix.provider || '') : ''
|
|
1018
|
+
useEffect(() => {
|
|
1019
|
+
if (!fix) return undefined
|
|
1020
|
+
let alive = true
|
|
1021
|
+
fetch(API + '/models')
|
|
1022
|
+
.then((r) => r.json())
|
|
1023
|
+
.then((j) => {
|
|
1024
|
+
if (alive && j && j.ok) setFixProviders(j.providers || [])
|
|
1025
|
+
})
|
|
1026
|
+
.catch(() => {})
|
|
1027
|
+
return () => {
|
|
1028
|
+
alive = false
|
|
1029
|
+
}
|
|
1030
|
+
}, [!!fix])
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
if (!fix || !fixProvider) return undefined
|
|
1033
|
+
let alive = true
|
|
1034
|
+
fetch(API + '/models?provider=' + encodeURIComponent(fixProvider))
|
|
1035
|
+
.then((r) => r.json())
|
|
1036
|
+
.then((j) => {
|
|
1037
|
+
if (alive && j && j.ok) setFixModels(j.models || [])
|
|
1038
|
+
})
|
|
1039
|
+
.catch(() => {})
|
|
1040
|
+
return () => {
|
|
1041
|
+
alive = false
|
|
1042
|
+
}
|
|
1043
|
+
}, [fixProvider])
|
|
1044
|
+
|
|
1045
|
+
const modelIdOf = (m) => String((m && typeof m === 'object' ? m.model || m.id : m) || '').trim().toLowerCase()
|
|
1046
|
+
const sameAsMain = () => {
|
|
1047
|
+
const main = swap.main[sidKey]
|
|
1048
|
+
const s = settings.value
|
|
1049
|
+
if (!main || !s || s.hideSameSourceModal) return false
|
|
1050
|
+
// 自动 Pro 档时,比的是后端最终会用的那个模型(settings.resolved),不是配置里那个旧 id
|
|
1051
|
+
const sub = s.modelAuto ? settings.resolved : s.model
|
|
1052
|
+
return !!modelIdOf(main) && modelIdOf(main) === modelIdOf(sub)
|
|
1053
|
+
}
|
|
1054
|
+
const startReview = (kind) => {
|
|
1055
|
+
const acked = !!(settings.value && settings.value.noticeAck)
|
|
1056
|
+
if (kind !== 'mock' && acked && sameAsMain()) {
|
|
1057
|
+
const s = settings.value
|
|
1058
|
+
setFix({
|
|
1059
|
+
provider: String(s.provider || ''),
|
|
1060
|
+
model: String(s.modelAuto ? settings.resolved || s.model || '' : s.model || ''),
|
|
1061
|
+
kind,
|
|
1062
|
+
})
|
|
1063
|
+
setFixNoMore(false)
|
|
1064
|
+
return
|
|
1065
|
+
}
|
|
1066
|
+
run(kind)
|
|
1067
|
+
}
|
|
1068
|
+
const closeFix = (save) => {
|
|
1069
|
+
const patch = { hideSameSourceModal: !!fixNoMore }
|
|
1070
|
+
if (save && fix) {
|
|
1071
|
+
patch.provider = fix.provider
|
|
1072
|
+
patch.model = fix.model
|
|
1073
|
+
patch.modelAuto = false
|
|
1074
|
+
}
|
|
1075
|
+
fetch(API + '/config', {
|
|
1076
|
+
method: 'POST',
|
|
1077
|
+
headers: { 'content-type': 'application/json' },
|
|
1078
|
+
body: JSON.stringify(patch),
|
|
1079
|
+
})
|
|
1080
|
+
.then((r) => r.json())
|
|
1081
|
+
.then((j) => {
|
|
1082
|
+
if (j && j.ok) {
|
|
1083
|
+
settings.value = j.config
|
|
1084
|
+
if (j.config && j.config.model) setReviewerName(String(j.config.model))
|
|
1085
|
+
}
|
|
1086
|
+
})
|
|
1087
|
+
.catch(() => {})
|
|
1088
|
+
const kind = fix && fix.kind
|
|
1089
|
+
setFix(null)
|
|
1090
|
+
if (save && kind) run(kind)
|
|
1091
|
+
}
|
|
1092
|
+
const fixOptions = (list, fallbackId) => {
|
|
1093
|
+
const arr = (list && list.length ? list : [{ id: fallbackId || '', name: fallbackId || '' }])
|
|
1094
|
+
.map((p) => (typeof p === 'string' ? { id: p, name: p } : { id: String((p && p.id) || ''), name: String((p && p.name) || (p && p.id) || '') }))
|
|
1095
|
+
.filter((p) => p.id)
|
|
1096
|
+
return arr.map((p) => React.createElement('option', { key: p.id, value: p.id }, p.name === p.id ? p.id : p.name + '(' + p.id + ')'))
|
|
1097
|
+
}
|
|
1098
|
+
const fixModal = () => {
|
|
1099
|
+
if (!fix) return null
|
|
1100
|
+
const dark = isDarkUI()
|
|
1101
|
+
const bg = dark ? '#151515' : '#fdfdfd'
|
|
1102
|
+
const fg = dark ? '#f3f3f3' : '#111'
|
|
1103
|
+
const sub = dark ? 'rgba(243,243,243,0.62)' : 'rgba(17,17,17,0.6)'
|
|
1104
|
+
const line = dark ? 'rgba(255,255,255,0.16)' : 'rgba(0,0,0,0.14)'
|
|
1105
|
+
const inputBg = dark ? '#1f1f1f' : '#fff'
|
|
1106
|
+
const selectStyle = {
|
|
1107
|
+
width: '100%',
|
|
1108
|
+
padding: '7px 8px',
|
|
1109
|
+
borderRadius: 8,
|
|
1110
|
+
border: '1px solid ' + line,
|
|
1111
|
+
background: inputBg,
|
|
1112
|
+
color: fg,
|
|
1113
|
+
fontSize: FS.small,
|
|
1114
|
+
marginTop: 6,
|
|
1115
|
+
}
|
|
1116
|
+
return React.createElement(
|
|
1117
|
+
'div',
|
|
1118
|
+
{
|
|
1119
|
+
// 把整页压暗,只留这一个窗是亮的
|
|
1120
|
+
style: {
|
|
1121
|
+
position: 'fixed',
|
|
1122
|
+
inset: 0,
|
|
1123
|
+
background: 'rgba(0,0,0,0.58)',
|
|
1124
|
+
display: 'flex',
|
|
1125
|
+
alignItems: 'center',
|
|
1126
|
+
justifyContent: 'center',
|
|
1127
|
+
zIndex: 9999,
|
|
1128
|
+
padding: 16,
|
|
1129
|
+
},
|
|
1130
|
+
onClick: (e) => {
|
|
1131
|
+
if (e && e.target === e.currentTarget) closeFix(false)
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
React.createElement(
|
|
1135
|
+
'div',
|
|
1136
|
+
{
|
|
1137
|
+
style: {
|
|
1138
|
+
width: 420,
|
|
1139
|
+
maxWidth: '100%',
|
|
1140
|
+
background: bg,
|
|
1141
|
+
color: fg,
|
|
1142
|
+
border: '1px solid ' + line,
|
|
1143
|
+
borderRadius: 12,
|
|
1144
|
+
padding: '18px 18px 14px',
|
|
1145
|
+
boxShadow: '0 24px 60px rgba(0,0,0,0.45)',
|
|
1146
|
+
fontSize: FS.body,
|
|
1147
|
+
lineHeight: 1.7,
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: '1.05em', marginBottom: 4 } }, '当前主副模型重合'),
|
|
1151
|
+
React.createElement(
|
|
1152
|
+
'div',
|
|
1153
|
+
{ style: { color: sub, fontSize: FS.small, marginBottom: 12 } },
|
|
1154
|
+
'副模型和主模型是同一个(' +
|
|
1155
|
+
String((swap.main[sidKey] && swap.main[sidKey].model) || fix.model || '') +
|
|
1156
|
+
'),等于让同一双眼睛检查自己 —— 复核效果不佳。换一个再评。',
|
|
1157
|
+
),
|
|
1158
|
+
React.createElement('div', { style: { color: sub, fontSize: FS.min, marginBottom: 2 } }, '服务商'),
|
|
1159
|
+
React.createElement(
|
|
1160
|
+
'select',
|
|
1161
|
+
{
|
|
1162
|
+
value: fix.provider || '',
|
|
1163
|
+
style: selectStyle,
|
|
1164
|
+
onChange: (e) => setFix({ ...fix, provider: e.target.value, model: '' }),
|
|
1165
|
+
},
|
|
1166
|
+
...fixOptions(fixProviders, fix.provider),
|
|
1167
|
+
),
|
|
1168
|
+
React.createElement('div', { style: { color: sub, fontSize: FS.min, margin: '10px 0 2px' } }, '副模型'),
|
|
1169
|
+
React.createElement(
|
|
1170
|
+
'select',
|
|
1171
|
+
{
|
|
1172
|
+
value: fix.model || '',
|
|
1173
|
+
style: selectStyle,
|
|
1174
|
+
onChange: (e) => setFix({ ...fix, model: e.target.value }),
|
|
1175
|
+
},
|
|
1176
|
+
fix.model ? null : React.createElement('option', { key: '__none', value: '' }, '(选一个)'),
|
|
1177
|
+
...fixOptions(fixModels, ''),
|
|
1178
|
+
),
|
|
1179
|
+
React.createElement(
|
|
1180
|
+
'label',
|
|
1181
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 12, color: sub, fontSize: FS.min, cursor: 'pointer' } },
|
|
1182
|
+
React.createElement('input', {
|
|
1183
|
+
type: 'checkbox',
|
|
1184
|
+
checked: !!fixNoMore,
|
|
1185
|
+
style: { accentColor: ACCENT },
|
|
1186
|
+
onChange: (e) => setFixNoMore(!!e.target.checked),
|
|
1187
|
+
}),
|
|
1188
|
+
'不再提示',
|
|
1189
|
+
),
|
|
1190
|
+
React.createElement(
|
|
1191
|
+
'div',
|
|
1192
|
+
{ style: { display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16 } },
|
|
1193
|
+
React.createElement(
|
|
1194
|
+
'button',
|
|
1195
|
+
{
|
|
1196
|
+
type: 'button',
|
|
1197
|
+
onClick: () => closeFix(false),
|
|
1198
|
+
style: {
|
|
1199
|
+
padding: '8px 16px',
|
|
1200
|
+
borderRadius: 8,
|
|
1201
|
+
background: 'transparent',
|
|
1202
|
+
color: fg,
|
|
1203
|
+
border: '1px solid ' + line,
|
|
1204
|
+
fontSize: FS.small,
|
|
1205
|
+
cursor: 'pointer',
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
'取消',
|
|
1209
|
+
),
|
|
1210
|
+
React.createElement(
|
|
1211
|
+
'button',
|
|
1212
|
+
{
|
|
1213
|
+
type: 'button',
|
|
1214
|
+
onClick: () => closeFix(true),
|
|
1215
|
+
style: {
|
|
1216
|
+
padding: '8px 16px',
|
|
1217
|
+
borderRadius: 8,
|
|
1218
|
+
background: ACCENT,
|
|
1219
|
+
color: '#fff',
|
|
1220
|
+
border: '1px solid transparent',
|
|
1221
|
+
fontSize: FS.small,
|
|
1222
|
+
fontWeight: 600,
|
|
1223
|
+
cursor: 'pointer',
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
'保存并评审',
|
|
1227
|
+
),
|
|
1228
|
+
),
|
|
1229
|
+
),
|
|
1230
|
+
)
|
|
1231
|
+
}
|
|
1232
|
+
|
|
918
1233
|
// 停止这次评审(0.9.0):不设总时长之后,"能随时喊停"就是新的保险丝。
|
|
919
1234
|
// 只给服务端发一个标志,流那边每一块之间看一眼就收手;已经花掉的调用收不回来。
|
|
920
1235
|
const stop = () => {
|
|
@@ -1087,6 +1402,23 @@ window.__ModuleLoader__.load({
|
|
|
1087
1402
|
? '已等 ' + secs + ' 秒,上限 ' + Math.round(budget / 1000) + ' 秒(真实时间,不是猜的)'
|
|
1088
1403
|
: '已等 ' + secs + ' 秒,不设上限(这条线只表示它还活着)',
|
|
1089
1404
|
}),
|
|
1405
|
+
// 碎碎念:一句一行地换,像有人在旁边用大白话垫着(到点/卡住/停止时不出现)
|
|
1406
|
+
!over && !stalling && !stopping
|
|
1407
|
+
? React.createElement(
|
|
1408
|
+
'div',
|
|
1409
|
+
{
|
|
1410
|
+
style: {
|
|
1411
|
+
marginTop: 6,
|
|
1412
|
+
fontSize: FS.min,
|
|
1413
|
+
opacity: 0.45,
|
|
1414
|
+
whiteSpace: 'nowrap',
|
|
1415
|
+
overflow: 'hidden',
|
|
1416
|
+
textOverflow: 'ellipsis',
|
|
1417
|
+
},
|
|
1418
|
+
},
|
|
1419
|
+
'· ' + THINK_LINES[quipIdx % THINK_LINES.length],
|
|
1420
|
+
)
|
|
1421
|
+
: null,
|
|
1090
1422
|
think
|
|
1091
1423
|
? React.createElement(
|
|
1092
1424
|
Collapse,
|
|
@@ -1115,6 +1447,20 @@ window.__ModuleLoader__.load({
|
|
|
1115
1447
|
)
|
|
1116
1448
|
}
|
|
1117
1449
|
|
|
1450
|
+
// 1.3.6:闸门跳过(没花钱,也别装成评过了)
|
|
1451
|
+
if (state === 'skipped') {
|
|
1452
|
+
return React.createElement(
|
|
1453
|
+
'div',
|
|
1454
|
+
{
|
|
1455
|
+
key: 'skipped',
|
|
1456
|
+
ref: grow,
|
|
1457
|
+
style: { marginTop: 10, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', fontSize: FS.min, opacity: 0.6 },
|
|
1458
|
+
},
|
|
1459
|
+
React.createElement('span', { style: { opacity: 0.9 } }, '自动评审跳过:' + String((entry && entry.note) || '这一轮没动过手').slice(0, 80)),
|
|
1460
|
+
React.createElement(Btn, { tier: 'lite', onClick: () => run('manual'), title: '这一轮还是评一次(会调用副模型、花钱)' }, '还是评一下'),
|
|
1461
|
+
)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1118
1464
|
if (state === 'error') {
|
|
1119
1465
|
const partial = String((entry && entry.partial) || '')
|
|
1120
1466
|
return React.createElement(
|
|
@@ -1337,9 +1683,25 @@ window.__ModuleLoader__.load({
|
|
|
1337
1683
|
entry && entry.version ? React.createElement('span', null, '· v' + entry.version) : null,
|
|
1338
1684
|
)
|
|
1339
1685
|
: null,
|
|
1686
|
+
usageChip(entry && entry.usage),
|
|
1687
|
+
// 1.3.5:副模型和主模型是同一个 → 这次复核基本等于自评,先说清楚(他 2026-11-11 要的)
|
|
1688
|
+
entry && entry.sameSource
|
|
1689
|
+
? React.createElement(
|
|
1690
|
+
'span',
|
|
1691
|
+
{
|
|
1692
|
+
title:
|
|
1693
|
+
'主模型和副模型是同一个模型' +
|
|
1694
|
+
(entry.main && entry.main.model ? '(' + String(entry.main.model) + ')' : '') +
|
|
1695
|
+
',独立视角接近于零。换一个不同来源、或至少不同档位的副模型才有用。',
|
|
1696
|
+
style: { color: '#f59e0b', fontSize: FS.min, whiteSpace: 'nowrap', opacity: 0.95 },
|
|
1697
|
+
},
|
|
1698
|
+
'与主模型相同 · 自评',
|
|
1699
|
+
)
|
|
1700
|
+
: null,
|
|
1340
1701
|
React.createElement('span', { style: { flex: 1, minWidth: 8 } }),
|
|
1341
1702
|
swapChip(),
|
|
1342
1703
|
),
|
|
1704
|
+
fixModal(),
|
|
1343
1705
|
)
|
|
1344
1706
|
}
|
|
1345
1707
|
|
|
@@ -1349,7 +1711,7 @@ window.__ModuleLoader__.load({
|
|
|
1349
1711
|
{ style: { marginTop: 10, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', fontSize: FS.body } },
|
|
1350
1712
|
React.createElement(
|
|
1351
1713
|
Btn,
|
|
1352
|
-
{ tier: 'pri', onClick: () =>
|
|
1714
|
+
{ tier: 'pri', onClick: () => startReview('manual'), title: '让 ' + (reviewerName || '副模型') + ' 挑一遍这一轮的毛病' },
|
|
1353
1715
|
React.createElement(Mark, { size: 14 }),
|
|
1354
1716
|
'二次评审',
|
|
1355
1717
|
),
|
|
@@ -1370,6 +1732,7 @@ window.__ModuleLoader__.load({
|
|
|
1370
1732
|
'(设置页没挂上:' + initErrors.join(';') + ')',
|
|
1371
1733
|
)
|
|
1372
1734
|
: null,
|
|
1735
|
+
fixModal(),
|
|
1373
1736
|
)
|
|
1374
1737
|
}
|
|
1375
1738
|
|
|
@@ -1440,6 +1803,8 @@ window.__ModuleLoader__.load({
|
|
|
1440
1803
|
String(c.provider || ''),
|
|
1441
1804
|
String(c.model || ''),
|
|
1442
1805
|
!!c.auto,
|
|
1806
|
+
String(c.autoMode || 'every'),
|
|
1807
|
+
c.modelAuto !== false,
|
|
1443
1808
|
!!c.mock,
|
|
1444
1809
|
!!c.allowWrite,
|
|
1445
1810
|
Number(c.budgetSec) || 0,
|
|
@@ -1508,6 +1873,8 @@ window.__ModuleLoader__.load({
|
|
|
1508
1873
|
provider: cfg.provider,
|
|
1509
1874
|
model: cfg.model,
|
|
1510
1875
|
auto: !!cfg.auto,
|
|
1876
|
+
autoMode: cfg.autoMode === 'actions' ? 'actions' : 'every',
|
|
1877
|
+
modelAuto: cfg.modelAuto !== false,
|
|
1511
1878
|
mock: !!cfg.mock,
|
|
1512
1879
|
allowWrite: !!cfg.allowWrite,
|
|
1513
1880
|
budgetSec: Number(cfg.budgetSec) || 0,
|
|
@@ -1547,10 +1914,30 @@ window.__ModuleLoader__.load({
|
|
|
1547
1914
|
: note || '改动不会自动保存'
|
|
1548
1915
|
const hintColor = failedSave ? '#ef4444' : dirty ? '#f59e0b' : 'inherit'
|
|
1549
1916
|
|
|
1917
|
+
/* 1.3.6:预设只改开关,绝不偷偷换模型 —— 最终决定权在用户手上(他 2026-11-11 的原话) */
|
|
1918
|
+
const applyPreset = (p) => {
|
|
1919
|
+
if (!cfg) return
|
|
1920
|
+
if (p === 'light') setCfg({ ...cfg, auto: true, autoMode: 'actions', mock: false })
|
|
1921
|
+
else if (p === 'delivery') setCfg({ ...cfg, auto: false, autoMode: 'every', mock: false })
|
|
1922
|
+
else if (p === 'peek') setCfg({ ...cfg, auto: false, autoMode: 'every', mock: true })
|
|
1923
|
+
setNote(p === 'peek' ? '已切到「只看不评」:接下来评的是假数据、不花钱' : '已套用预设 —— 点下面的保存才生效')
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1550
1926
|
return React.createElement(
|
|
1551
1927
|
'div',
|
|
1552
1928
|
{ style: { padding: '4px 0', maxWidth: 620 } },
|
|
1553
1929
|
|
|
1930
|
+
// 1.3.6:三个预设摆最上面,装完不用读文档也能选
|
|
1931
|
+
React.createElement(
|
|
1932
|
+
'div',
|
|
1933
|
+
{ style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', margin: '0 0 14px' } },
|
|
1934
|
+
React.createElement('span', { style: { opacity: 0.7, minWidth: 88 } }, '预设'),
|
|
1935
|
+
React.createElement(Btn, { tier: 'sec', onClick: () => applyPreset('light'), title: '日常:不每条都评,只有它动过手(写文件、跑命令、发消息)才自动评' }, '日常轻量'),
|
|
1936
|
+
React.createElement(Btn, { tier: 'sec', onClick: () => applyPreset('delivery'), title: '关键交付:不自动评,你点哪一轮评哪一轮' }, '关键交付'),
|
|
1937
|
+
React.createElement(Btn, { tier: 'lite', onClick: () => applyPreset('peek'), title: '先看看卡片长什么样:假数据、不花钱、不留记录' }, '只看不评'),
|
|
1938
|
+
React.createElement('span', { style: { opacity: 0.45, fontSize: FS.min } }, '只改开关,模型还是你自己选'),
|
|
1939
|
+
),
|
|
1940
|
+
|
|
1554
1941
|
section(
|
|
1555
1942
|
'01',
|
|
1556
1943
|
'模型',
|
|
@@ -1566,7 +1953,7 @@ window.__ModuleLoader__.load({
|
|
|
1566
1953
|
Select,
|
|
1567
1954
|
{
|
|
1568
1955
|
value: cfg.provider || '',
|
|
1569
|
-
onChange: (e) => setCfg({ ...cfg, provider: e.target.value, model: '' }),
|
|
1956
|
+
onChange: (e) => setCfg({ ...cfg, provider: e.target.value, model: '', modelAuto: true }),
|
|
1570
1957
|
},
|
|
1571
1958
|
...providerOptions.map((p) =>
|
|
1572
1959
|
React.createElement('option', { key: p.id, value: p.id }, p.name === p.id ? p.id : p.name + '(' + p.id + ')'),
|
|
@@ -1575,10 +1962,16 @@ window.__ModuleLoader__.load({
|
|
|
1575
1962
|
React.createElement(
|
|
1576
1963
|
Select,
|
|
1577
1964
|
{
|
|
1578
|
-
|
|
1579
|
-
|
|
1965
|
+
// 1.3.6:默认「自动 Pro 档」—— 不存具体 id,DSH 升了版本它自己跟着走
|
|
1966
|
+
value: cfg.modelAuto === false ? cfg.model || '' : '__auto__',
|
|
1967
|
+
onChange: (e) => {
|
|
1968
|
+
const v = e.target.value
|
|
1969
|
+
if (v === '__auto__') setCfg({ ...cfg, modelAuto: true })
|
|
1970
|
+
else setCfg({ ...cfg, model: v, modelAuto: false })
|
|
1971
|
+
},
|
|
1580
1972
|
},
|
|
1581
|
-
|
|
1973
|
+
React.createElement('option', { key: '__auto__', value: '__auto__' }, '自动:Pro 档'),
|
|
1974
|
+
cfg.modelAuto === false && cfg.model ? null : React.createElement('option', { key: '__none', value: '' }, '(手动选一个)'),
|
|
1582
1975
|
...models
|
|
1583
1976
|
.map((m) => (typeof m === 'string' ? { id: m, name: m } : { id: String((m && m.id) || ''), name: String((m && m.name) || (m && m.id) || '') }))
|
|
1584
1977
|
.filter((m) => m.id)
|
|
@@ -1587,6 +1980,14 @@ window.__ModuleLoader__.load({
|
|
|
1587
1980
|
),
|
|
1588
1981
|
),
|
|
1589
1982
|
React.createElement('div', { style: { opacity: 0.55, fontSize: FS.min, marginTop: 6 } }, '评审由这个模型来做;「副模型主刀」打开时,下一轮答复也换成它。'),
|
|
1983
|
+
// 1.3.6:默认 Pro 档、自动跟随;手动选了别的就按你选的来
|
|
1984
|
+
React.createElement(
|
|
1985
|
+
'div',
|
|
1986
|
+
{ style: { opacity: 0.5, fontSize: FS.min, marginTop: 2 } },
|
|
1987
|
+
cfg.modelAuto === false
|
|
1988
|
+
? '当前是手动指定:' + String(cfg.model || '未选') + '。想省事可以切回「自动:Pro 档」,DSH 升级后它自己跟着换。'
|
|
1989
|
+
: '默认走 Pro 档(' + String((models.find((m) => /pro/i.test(String((m && m.id) || m))) || {}).id || '按你的 provider 实际清单') + '),DSH 出了新一代 Pro 会自动跟着换;主模型如果也是它,这次复核等于自评。',
|
|
1990
|
+
),
|
|
1590
1991
|
),
|
|
1591
1992
|
|
|
1592
1993
|
section(
|
|
@@ -1595,10 +1996,34 @@ window.__ModuleLoader__.load({
|
|
|
1595
1996
|
React.createElement(Check, {
|
|
1596
1997
|
checked: cfg.auto,
|
|
1597
1998
|
onChange: (e) => setCfg({ ...cfg, auto: e.target.checked }),
|
|
1598
|
-
title: '
|
|
1599
|
-
desc: '
|
|
1999
|
+
title: '自动评审',
|
|
2000
|
+
desc: '会调用副模型,产生费用;评到哪一步由下面那行决定',
|
|
1600
2001
|
style: { marginBottom: 2 },
|
|
1601
2002
|
}),
|
|
2003
|
+
// 1.3.6:闸门 —— 用户决定"每条都评"还是"只有动过手才评"
|
|
2004
|
+
React.createElement(
|
|
2005
|
+
'div',
|
|
2006
|
+
{ style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap', margin: '2px 0 4px' } },
|
|
2007
|
+
React.createElement('span', { style: { opacity: 0.7, minWidth: 88 } }, '自动评审范围'),
|
|
2008
|
+
React.createElement(
|
|
2009
|
+
'span',
|
|
2010
|
+
{ style: { marginLeft: 'auto' } },
|
|
2011
|
+
React.createElement(
|
|
2012
|
+
Select,
|
|
2013
|
+
{
|
|
2014
|
+
value: cfg.autoMode === 'actions' ? 'actions' : 'every',
|
|
2015
|
+
onChange: (e) => setCfg({ ...cfg, autoMode: e.target.value }),
|
|
2016
|
+
},
|
|
2017
|
+
React.createElement('option', { key: 'every', value: 'every' }, '每条回复都评(默认)'),
|
|
2018
|
+
React.createElement('option', { key: 'actions', value: 'actions' }, '只有动过手的那一轮才评'),
|
|
2019
|
+
),
|
|
2020
|
+
),
|
|
2021
|
+
),
|
|
2022
|
+
React.createElement(
|
|
2023
|
+
'div',
|
|
2024
|
+
{ style: { opacity: 0.5, fontSize: FS.min, marginBottom: 6 } },
|
|
2025
|
+
'「动过手」=写文件、跑命令、发消息,或这一轮工作区有改动;纯聊天、纯只读的问答不评。判断全在本地,不花钱。',
|
|
2026
|
+
),
|
|
1602
2027
|
React.createElement(Check, {
|
|
1603
2028
|
checked: cfg.mock,
|
|
1604
2029
|
onChange: (e) => setCfg({ ...cfg, mock: e.target.checked }),
|
package/index.js
CHANGED
|
@@ -89,14 +89,20 @@ const MAX_CHARS = 60000
|
|
|
89
89
|
const LEGACY_BUDGET_MS = 45000
|
|
90
90
|
|
|
91
91
|
/* 版本号(0.7.0 起):/config 会带出去,设置页「关于」直接显示,不用去翻 package.json */
|
|
92
|
-
const VERSION = '1.3.
|
|
92
|
+
const VERSION = '1.3.6'
|
|
93
93
|
|
|
94
94
|
/* ---------------- 配置(副模型 + 自动评审 + 主刀) ---------------- */
|
|
95
95
|
|
|
96
96
|
const DEFAULT_CONFIG = {
|
|
97
97
|
provider: 'deepseek-account',
|
|
98
|
-
|
|
98
|
+
/* 1.3.6:副模型默认走 Pro 档,而且"跟着版本走" —— modelAuto 打开时不用存具体 id,
|
|
99
|
+
每次评审现查这个 provider 的模型清单,挑名字里带 pro 的最新一代。
|
|
100
|
+
DSH 哪天把 DeepSeek-V4-Pro 换成 V4.1-Pro,装插件的人不用改设置也跟着变。 */
|
|
101
|
+
model: 'deepseek-v4-pro',
|
|
102
|
+
modelAuto: true,
|
|
99
103
|
auto: false,
|
|
104
|
+
// 1.3.6:重合提示弹窗的"不再提示"(只在用户自己勾了才为 true)
|
|
105
|
+
hideSameSourceModal: false,
|
|
100
106
|
mock: false,
|
|
101
107
|
/* 副模型能不能改文件:默认关(2026-11-09 他选的 B)。
|
|
102
108
|
关着的时候工具表里根本没有 write_file,模型连"想调"都无从下手;
|
|
@@ -115,6 +121,10 @@ const DEFAULT_CONFIG = {
|
|
|
115
121
|
她把一条自己编出来的用户消息当成用户说的,还照着它删了信、改了记忆。
|
|
116
122
|
纯本地字符串比对 —— 不调模型、不花钱、不拖慢评审。 */
|
|
117
123
|
checkQuotes: true,
|
|
124
|
+
/* 1.3.6:自动评审走到哪一步才触发(用户自己选,插件只负责执行)。
|
|
125
|
+
'every' = 每条回复都评(1.3.5 及以前的行为,默认,别偷偷改人家的习惯)
|
|
126
|
+
'actions' = 只有这一轮真动过手(写文件/跑命令/发消息/工作区有变化)才评 */
|
|
127
|
+
autoMode: 'every',
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
const CONFIG_FILE = path.join(os.homedir(), '.dsh', 'dsh-second-review', 'config.json')
|
|
@@ -927,6 +937,144 @@ export function verifyUserQuotes(rows) {
|
|
|
927
937
|
return result
|
|
928
938
|
}
|
|
929
939
|
|
|
940
|
+
/* 1.3.5:主模型和副模型是不是"同一个"。
|
|
941
|
+
只判完全相同这一层:同一个 model 换个 provider,还是同一双眼睛;
|
|
942
|
+
不同 provider 的模型命名各异,硬猜"同源"会误报(小问 2026-11-10 的判断,我认同)。
|
|
943
|
+
只用来提示,不用来拦截:只有一个 Key 的人照样允许重选。 */
|
|
944
|
+
export function sameModel(a, b) {
|
|
945
|
+
const pick = (x) => String((x && typeof x === 'object' ? x.model : x) || '').trim().toLowerCase()
|
|
946
|
+
const am = pick(a)
|
|
947
|
+
const bm = pick(b)
|
|
948
|
+
return !!am && am === bm
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/* 1.3.6:Pro 档自动跟随 —— 从模型清单里挑名字带 pro 的最新一版。
|
|
952
|
+
查不到清单/清单里没有 pro,就退回配置里存的 id,绝不乱猜、也不擅自换成便宜档。 */
|
|
953
|
+
export function pickProModel(list) {
|
|
954
|
+
const ids = (Array.isArray(list) ? list : [])
|
|
955
|
+
.map((m) => String((m && typeof m === 'object' ? m.id : m) || ''))
|
|
956
|
+
.filter((id) => id && /pro/i.test(id))
|
|
957
|
+
if (!ids.length) return ''
|
|
958
|
+
const ver = (id) => (id.match(/\d+(?:[._]\d+)*/g) || []).join('.').split(/[._]/).filter(Boolean).map(Number)
|
|
959
|
+
ids.sort((a, b) => {
|
|
960
|
+
const va = ver(a)
|
|
961
|
+
const vb = ver(b)
|
|
962
|
+
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
|
|
963
|
+
const x = va[i] || 0
|
|
964
|
+
const y = vb[i] || 0
|
|
965
|
+
if (x !== y) return y - x
|
|
966
|
+
}
|
|
967
|
+
return a.localeCompare(b)
|
|
968
|
+
})
|
|
969
|
+
return ids[0]
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
export async function resolveReviewModel(llm, cfg) {
|
|
973
|
+
if (!cfg || cfg.modelAuto !== true) return cfg && cfg.model
|
|
974
|
+
try {
|
|
975
|
+
const list = await llm.listModels(cfg.provider)
|
|
976
|
+
const pro = pickProModel(list)
|
|
977
|
+
if (pro) return pro
|
|
978
|
+
} catch {}
|
|
979
|
+
return cfg.model
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/* ---------------- 1.3.6:会话说得清的账,都从日志里算,不再问模型 ----------------
|
|
983
|
+
会话日志里每一轮事件都带 seq / turn,assistant/message 上带 usage:
|
|
984
|
+
{inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, totalTokens}
|
|
985
|
+
(2026-11-11 从本机真实会话解出来验过)。所以"用了多少 token"可以纯代码算,
|
|
986
|
+
不额外调一次模型、不花钱。 */
|
|
987
|
+
|
|
988
|
+
function logItems(session) {
|
|
989
|
+
const log = (session && session.log) || {}
|
|
990
|
+
return Object.entries(log)
|
|
991
|
+
.map(([k, e]) => ({ seq: Number(e && e.seq) || Number(k), e }))
|
|
992
|
+
.filter((x) => Number.isFinite(x.seq))
|
|
993
|
+
.sort((a, b) => a.seq - b.seq)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/* 被评的那条回复属于第几轮(turn) */
|
|
997
|
+
export function turnOf(session, beforeSeq) {
|
|
998
|
+
const limit = Number(beforeSeq)
|
|
999
|
+
let turn = null
|
|
1000
|
+
for (const { seq, e } of logItems(session)) {
|
|
1001
|
+
if (Number.isFinite(limit) && seq > limit) break
|
|
1002
|
+
if (e && e.type === 'assistant/message' && e.data && typeof e.data.turn === 'number') turn = e.data.turn
|
|
1003
|
+
}
|
|
1004
|
+
return turn
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/* 这一轮主模型花了多少 token(多步/多轮的都按 turn 加总) */
|
|
1008
|
+
export function usageOfTurn(session, beforeSeq) {
|
|
1009
|
+
const turn = turnOf(session, beforeSeq)
|
|
1010
|
+
if (turn === null) return null
|
|
1011
|
+
const sum = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, messages: 0 }
|
|
1012
|
+
for (const { e } of logItems(session)) {
|
|
1013
|
+
if (!e || e.type !== 'assistant/message' || !e.data || e.data.turn !== turn) continue
|
|
1014
|
+
const u = e.data.usage
|
|
1015
|
+
if (!u) continue
|
|
1016
|
+
sum.input += Number(u.inputTokens) || 0
|
|
1017
|
+
sum.output += Number(u.outputTokens) || 0
|
|
1018
|
+
sum.cacheRead += Number(u.cacheReadTokens) || 0
|
|
1019
|
+
sum.cacheWrite += Number(u.cacheWriteTokens) || 0
|
|
1020
|
+
sum.total += Number(u.totalTokens) || 0
|
|
1021
|
+
sum.messages += 1
|
|
1022
|
+
}
|
|
1023
|
+
return sum.messages ? sum : null
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/* 这一轮到底动没动手:写文件/跑命令/发消息/工作区有变化才算。
|
|
1027
|
+
纯聊天、纯只读的问答不算 —— 这正是"省钱的闸门"的判据。 */
|
|
1028
|
+
const ACTION_TOOL_RE = /(write|edit|delete|remove|move|rename|mkdir|patch|shell|bash|pwsh|powershell|exec|spawn|install|upload|send|post|publish|run)/i
|
|
1029
|
+
|
|
1030
|
+
export function turnTouched(session, beforeSeq) {
|
|
1031
|
+
const turn = turnOf(session, beforeSeq)
|
|
1032
|
+
if (turn === null) return false
|
|
1033
|
+
let touched = false
|
|
1034
|
+
for (const { e } of logItems(session)) {
|
|
1035
|
+
if (!e || !e.data || e.data.turn !== turn) continue
|
|
1036
|
+
if (e.type === 'workspace/changes') return true
|
|
1037
|
+
if (e.type === 'tool/call' && ACTION_TOOL_RE.test(String(e.data.name || ''))) touched = true
|
|
1038
|
+
}
|
|
1039
|
+
return touched
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/* 评审自己那几次调用,finish 里要是带 usage 就取出来;没带就返回 null(不编数字)。 */
|
|
1043
|
+
export function usageOfFinish(finish) {
|
|
1044
|
+
if (!finish || typeof finish !== 'object') return null
|
|
1045
|
+
const u =
|
|
1046
|
+
finish.usage ||
|
|
1047
|
+
(finish.message && finish.message.usage) ||
|
|
1048
|
+
(finish.result && finish.result.usage) ||
|
|
1049
|
+
(finish.data && finish.data.usage) ||
|
|
1050
|
+
null
|
|
1051
|
+
if (!u || typeof u !== 'object') return null
|
|
1052
|
+
const n = (x) => Number(x) || 0
|
|
1053
|
+
const input = n(u.inputTokens || u.input_tokens || u.promptTokens)
|
|
1054
|
+
const output = n(u.outputTokens || u.output_tokens || u.completionTokens)
|
|
1055
|
+
const cacheRead = n(u.cacheReadTokens || u.cache_read_tokens)
|
|
1056
|
+
const cacheWrite = n(u.cacheWriteTokens || u.cache_write_tokens)
|
|
1057
|
+
if (!input && !output && !cacheRead && !cacheWrite) return null
|
|
1058
|
+
return { input, output, cacheRead, cacheWrite, total: input + output }
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/* 把一次评审里每一步的用量加起来(带工具时会有好几次调用) */
|
|
1062
|
+
export function sumUsage(list) {
|
|
1063
|
+
if (!Array.isArray(list) || !list.length) return null
|
|
1064
|
+
const sum = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
1065
|
+
let any = false
|
|
1066
|
+
for (const u of list) {
|
|
1067
|
+
if (!u) continue
|
|
1068
|
+
any = true
|
|
1069
|
+
sum.input += Number(u.input) || 0
|
|
1070
|
+
sum.output += Number(u.output) || 0
|
|
1071
|
+
sum.cacheRead += Number(u.cacheRead) || 0
|
|
1072
|
+
sum.cacheWrite += Number(u.cacheWrite) || 0
|
|
1073
|
+
sum.total += Number(u.total) || 0
|
|
1074
|
+
}
|
|
1075
|
+
return any ? sum : null
|
|
1076
|
+
}
|
|
1077
|
+
|
|
930
1078
|
export function saveReview(rootDir, review, steps, meta) {
|
|
931
1079
|
try {
|
|
932
1080
|
const dir = path.join(rootDir, '二次评审')
|
|
@@ -1249,14 +1397,22 @@ export function apply(ctx) {
|
|
|
1249
1397
|
if (body && typeof body.mock === 'boolean') patch.mock = body.mock
|
|
1250
1398
|
if (body && typeof body.allowWrite === 'boolean') patch.allowWrite = body.allowWrite
|
|
1251
1399
|
if (body && typeof body.noticeAck === 'boolean') patch.noticeAck = body.noticeAck
|
|
1400
|
+
// 1.3.6:Pro 档自动跟随;重合弹窗的"不再提示"
|
|
1401
|
+
if (body && typeof body.modelAuto === 'boolean') patch.modelAuto = body.modelAuto
|
|
1402
|
+
if (body && typeof body.hideSameSourceModal === 'boolean') patch.hideSameSourceModal = body.hideSameSourceModal
|
|
1252
1403
|
// 0.9.0:两个闸都能从设置页改,夹到合理区间(0 = 不设)
|
|
1253
1404
|
if (body && Number.isFinite(Number(body.budgetSec))) patch.budgetSec = Math.max(0, Math.min(1800, Math.round(Number(body.budgetSec))))
|
|
1254
1405
|
if (body && Number.isFinite(Number(body.idleSec))) patch.idleSec = Math.max(0, Math.min(1800, Math.round(Number(body.idleSec))))
|
|
1255
1406
|
// 1.1.0:引文核验的开关(默认开,纯本地比对,不花钱)
|
|
1256
1407
|
if (body && typeof body.checkQuotes === 'boolean') patch.checkQuotes = body.checkQuotes
|
|
1257
|
-
|
|
1408
|
+
// 1.3.6:自动评审范围,只认这两个值(别让乱七八糟的值把闸门关死)
|
|
1409
|
+
if (body && (body.autoMode === 'every' || body.autoMode === 'actions')) patch.autoMode = body.autoMode
|
|
1410
|
+
const saved = writeConfig(patch)
|
|
1411
|
+
// 1.3.6:顺手把"自动跟着走"最终落到哪个模型告诉前端(设置页和卡片都要显示它)
|
|
1412
|
+
return sendJson(res, 200, { ok: true, config: saved, resolvedModel: await resolveReviewModel(scope.get('llm'), saved), file: CONFIG_FILE, version: VERSION })
|
|
1258
1413
|
}
|
|
1259
|
-
|
|
1414
|
+
const cfg = readConfig()
|
|
1415
|
+
return sendJson(res, 200, { ok: true, config: cfg, resolvedModel: await resolveReviewModel(scope.get('llm'), cfg), file: CONFIG_FILE, version: VERSION })
|
|
1260
1416
|
} catch (error) {
|
|
1261
1417
|
return sendErr(res, error)
|
|
1262
1418
|
}
|
|
@@ -1306,6 +1462,29 @@ export function apply(ctx) {
|
|
|
1306
1462
|
}
|
|
1307
1463
|
}
|
|
1308
1464
|
|
|
1465
|
+
/* 1.3.5:这个会话"现在的主模型"是谁。
|
|
1466
|
+
走 DSH 自己的 agent.model.current()(副模型主刀也用它);
|
|
1467
|
+
读不到就返回 null —— 卡片上不显示提示,绝不为了一句提示去猜。 */
|
|
1468
|
+
const currentMain = (sid) => {
|
|
1469
|
+
try {
|
|
1470
|
+
const agents = scope.get('agents')
|
|
1471
|
+
const live = agents && typeof agents.list === 'function' ? agents.list() : []
|
|
1472
|
+
const normId = (v) => String(v || '').replace(/^session-/, '')
|
|
1473
|
+
const hit = (Array.isArray(live) ? live : []).find((a) => {
|
|
1474
|
+
try {
|
|
1475
|
+
return a && (normId(a.id) === normId(sid) || (a.session && normId(a.session.id) === normId(sid)))
|
|
1476
|
+
} catch {
|
|
1477
|
+
return false
|
|
1478
|
+
}
|
|
1479
|
+
})
|
|
1480
|
+
const cur = hit && hit.model && typeof hit.model.current === 'function' ? hit.model.current() : null
|
|
1481
|
+
if (!cur) return null
|
|
1482
|
+
return { provider: String(cur.provider || ''), model: String(cur.model || '') }
|
|
1483
|
+
} catch {
|
|
1484
|
+
return null
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1309
1488
|
// 副模型主刀:把这一轮"谁在答"换成副模型(走 DSH 官方的 agent.model.select,不碰消息归属)
|
|
1310
1489
|
// 状态按会话分开存:A 会话开了主刀,不会把 B 会话的"原来的模型"覆盖掉。
|
|
1311
1490
|
const swap = async (req, res) => {
|
|
@@ -1383,6 +1562,8 @@ export function apply(ctx) {
|
|
|
1383
1562
|
if (!llm) return sendJson(res, 503, { ok: false, error: 'llm service missing' })
|
|
1384
1563
|
|
|
1385
1564
|
let rows = []
|
|
1565
|
+
// 1.3.6:闸门和用量都要会话原始日志,先把找到的那份留住(demo 探针没有,就是 null)
|
|
1566
|
+
let sessionObj = null
|
|
1386
1567
|
const cfg = readConfig()
|
|
1387
1568
|
|
|
1388
1569
|
// 进度:前端带着 job=来的,就边跑边往里写"想什么"(只进内存,不进记录)
|
|
@@ -1423,8 +1604,11 @@ export function apply(ctx) {
|
|
|
1423
1604
|
const entry = sidKey ? readSwaps(cfg)[sidKey] : null
|
|
1424
1605
|
const reviewCfg =
|
|
1425
1606
|
!demo && entry && entry.saved && entry.saved.provider && entry.saved.model
|
|
1426
|
-
? { ...cfg, provider: entry.saved.provider, model: entry.saved.model }
|
|
1607
|
+
? { ...cfg, provider: entry.saved.provider, model: entry.saved.model, modelAuto: false }
|
|
1427
1608
|
: cfg
|
|
1609
|
+
// 1.3.6:Pro 档跟着版本走(只在 modelAuto 打开时生效;主刀互换时用的是原主模型,不动)
|
|
1610
|
+
const runModel = await resolveReviewModel(llm, reviewCfg)
|
|
1611
|
+
const runCfg = runModel && runModel !== reviewCfg.model ? { ...reviewCfg, model: runModel } : reviewCfg
|
|
1428
1612
|
|
|
1429
1613
|
let rootDir = null
|
|
1430
1614
|
if (demo) {
|
|
@@ -1459,6 +1643,7 @@ export function apply(ctx) {
|
|
|
1459
1643
|
if (hit && hit.session) {
|
|
1460
1644
|
rootDir = makeRoot(rootOf(hit.session))
|
|
1461
1645
|
rows = transcriptOf(hit.session, anchor)
|
|
1646
|
+
sessionObj = hit.session
|
|
1462
1647
|
} else {
|
|
1463
1648
|
return sendJson(res, 404, {
|
|
1464
1649
|
ok: false,
|
|
@@ -1469,9 +1654,21 @@ export function apply(ctx) {
|
|
|
1469
1654
|
} else {
|
|
1470
1655
|
rootDir = makeRoot(rootOf(session))
|
|
1471
1656
|
rows = transcriptOf(session, anchor)
|
|
1657
|
+
sessionObj = session
|
|
1472
1658
|
}
|
|
1473
1659
|
}
|
|
1474
1660
|
|
|
1661
|
+
/* 1.3.6:闸门。用户把 autoMode 设成 'actions' 时,纯聊天的轮次不调副模型 —— 直接跳过。
|
|
1662
|
+
判断全在本地(看这轮有没有写文件/跑命令/发消息),不花钱、不猜。 */
|
|
1663
|
+
if (!demo && body.auto === true && String(cfg.autoMode || 'every') === 'actions' && !turnTouched(sessionObj, anchor)) {
|
|
1664
|
+
return sendJson(res, 200, {
|
|
1665
|
+
ok: true,
|
|
1666
|
+
skipped: true,
|
|
1667
|
+
reason: '这一轮没动过手(没写文件、没跑命令、没发消息),自动评审跳过了',
|
|
1668
|
+
gate: 'actions',
|
|
1669
|
+
})
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1475
1672
|
if (rows.length === 0) return sendJson(res, 200, { ok: true, empty: true, verdict: 'pass', note: '这一轮还没有对话内容', major: [], minor: [], steps: 0 })
|
|
1476
1673
|
|
|
1477
1674
|
// 引文核验(1.1.0):在动模型之前先把"有没有编出来的用户原话"查一遍 —— 纯本地,零成本
|
|
@@ -1493,6 +1690,8 @@ export function apply(ctx) {
|
|
|
1493
1690
|
const allowWrite = cfg.allowWrite === true
|
|
1494
1691
|
const steps = []
|
|
1495
1692
|
const finishes = []
|
|
1693
|
+
// 1.3.6:评审自己每次调用的用量(finish 里带就收,不带就空着,绝不估算)
|
|
1694
|
+
const usages = []
|
|
1496
1695
|
let finalText = ''
|
|
1497
1696
|
let budget = 0
|
|
1498
1697
|
let timedOut = false
|
|
@@ -1523,7 +1722,8 @@ export function apply(ctx) {
|
|
|
1523
1722
|
break
|
|
1524
1723
|
}
|
|
1525
1724
|
// 工具不设次数上限(他 2026-11-09 明确说过:磨洋工是"看不该看的、说不该说的",不是限次数)
|
|
1526
|
-
const r = await streamWithRetry(llm, messages, tools,
|
|
1725
|
+
const r = await streamWithRetry(llm, messages, tools, runCfg, onDelta, deadlineAt, stopOpts)
|
|
1726
|
+
usages.push(usageOfFinish(r.finish))
|
|
1527
1727
|
finishes.push({
|
|
1528
1728
|
step: i,
|
|
1529
1729
|
kind: r.finish && r.finish.kind,
|
|
@@ -1551,7 +1751,7 @@ export function apply(ctx) {
|
|
|
1551
1751
|
messages.push({
|
|
1552
1752
|
role: 'assistant',
|
|
1553
1753
|
id: randomUUID(),
|
|
1554
|
-
source: { kind: 'assistant', provider:
|
|
1754
|
+
source: { kind: 'assistant', provider: runCfg.provider, model: runCfg.model },
|
|
1555
1755
|
...(r.replayState ? { replayState: r.replayState } : {}),
|
|
1556
1756
|
content: blocks,
|
|
1557
1757
|
})
|
|
@@ -1650,12 +1850,16 @@ export function apply(ctx) {
|
|
|
1650
1850
|
const swapped = !demo && !!(entry && entry.saved && entry.saved.provider && entry.saved.model)
|
|
1651
1851
|
const reviewer = {
|
|
1652
1852
|
provider: String(reviewCfg.provider || ''),
|
|
1653
|
-
model: String(
|
|
1853
|
+
model: String(runCfg.model || ''),
|
|
1654
1854
|
role: swapped ? '原主模型(副模型主刀时互换)' : '副模型',
|
|
1655
1855
|
}
|
|
1856
|
+
/* 1.3.5:这一评是"换了个脑子",还是"同一双眼睛又看了一遍"?
|
|
1857
|
+
主模型从 DSH 的 agent.model.current() 现取;读不到就是 null,卡片上不提示 —— 不猜。 */
|
|
1858
|
+
const main = demo ? null : currentMain(sessionId)
|
|
1859
|
+
const sameSource = !!(main && sameModel(main, reviewer))
|
|
1656
1860
|
const saved = demo
|
|
1657
1861
|
? ''
|
|
1658
|
-
: saveReview(rootDir, parsed, steps, { sessionId, seq: anchor, model:
|
|
1862
|
+
: saveReview(rootDir, parsed, steps, { sessionId, seq: anchor, model: runCfg.provider + ' / ' + runCfg.model, reviewer, provenance })
|
|
1659
1863
|
const payload = {
|
|
1660
1864
|
ok: true,
|
|
1661
1865
|
topic: parsed.topic,
|
|
@@ -1670,12 +1874,20 @@ export function apply(ctx) {
|
|
|
1670
1874
|
items: [...parsed.major, ...parsed.minor],
|
|
1671
1875
|
saved,
|
|
1672
1876
|
reviewer,
|
|
1877
|
+
// 1.3.5:主模型是谁、和副模型是不是同一个(同一个=这次复核基本是自评)
|
|
1878
|
+
main,
|
|
1879
|
+
sameSource,
|
|
1673
1880
|
// 1.1.0:引文核验结果(机器查的,不靠模型)。checked=0 就是这一轮它没引你的话
|
|
1674
1881
|
provenance,
|
|
1675
1882
|
// 这张卡是哪个版本画出来的(0.9.0 加)—— 他遇到过一次"改了没生效"
|
|
1676
1883
|
version: VERSION,
|
|
1677
1884
|
// 这一轮到底花了多久(0.9.1):5 分钟那种,卡上得看得见
|
|
1678
1885
|
elapsedMs: Date.now() - startedAt,
|
|
1886
|
+
// 1.3.6:这一轮主模型的用量(从会话日志算,零成本)+ 复核自己的用量(finish 带才算)
|
|
1887
|
+
usage: {
|
|
1888
|
+
turn: demo ? null : usageOfTurn(sessionObj, anchor),
|
|
1889
|
+
review: sumUsage(usages),
|
|
1890
|
+
},
|
|
1679
1891
|
parseFailed: parsed.parseFailed,
|
|
1680
1892
|
truncated,
|
|
1681
1893
|
timedOut,
|
package/package.json
CHANGED
package/test/client.smoke.mjs
CHANGED
|
@@ -102,6 +102,11 @@ function barOf(el) {
|
|
|
102
102
|
const bar = walk(el).find((n) => n.props && n.props.style && n.props.style.height === 2)
|
|
103
103
|
return bar && bar.kids && bar.kids[0]
|
|
104
104
|
}
|
|
105
|
+
function tiersOf(el) {
|
|
106
|
+
return walk(el)
|
|
107
|
+
.filter((n) => typeof n.type === 'function' && n.type.name === 'Btn')
|
|
108
|
+
.map((n) => n.props.tier)
|
|
109
|
+
}
|
|
105
110
|
function assert(cond, msg) {
|
|
106
111
|
if (!cond) throw new Error(msg)
|
|
107
112
|
}
|
|
@@ -132,11 +137,27 @@ const cases = [
|
|
|
132
137
|
// 1.1.0:引文核验(机器查的,不靠模型)
|
|
133
138
|
provenance: { checked: 2, found: 1, missing: ['我什么时候说让你回信了'], userCount: 6 },
|
|
134
139
|
reviewer: { provider: 'deepseek-account', model: 'deepseek-flash', role: '副模型' },
|
|
140
|
+
// 1.3.6:这一次的账(复核+本轮)
|
|
141
|
+
usage: { review: { input: 1234, output: 567, cacheRead: 0 }, turn: { input: 1534, output: 443, cacheRead: 14720 } },
|
|
135
142
|
},
|
|
136
143
|
],
|
|
137
144
|
['完成 通过', { state: 'done', verdict: 'pass', note: '没发现必须改的地方。', major: [], minor: [] }],
|
|
145
|
+
[
|
|
146
|
+
'完成 与主模型相同',
|
|
147
|
+
{
|
|
148
|
+
state: 'done',
|
|
149
|
+
verdict: 'pass',
|
|
150
|
+
note: '',
|
|
151
|
+
major: [],
|
|
152
|
+
minor: [],
|
|
153
|
+
reviewer: { provider: 'deepseek-account', model: 'deepseek-flash', role: '副模型' },
|
|
154
|
+
main: { provider: 'deepseek-account', model: 'deepseek-flash' },
|
|
155
|
+
sameSource: true,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
138
158
|
['完成 说不清', { state: 'done', verdict: 'unclear', note: '它说不清这一轮在评什么', major: [], minor: [] }],
|
|
139
159
|
['失败', { state: 'error', note: 'provider 断流' }],
|
|
160
|
+
['闸门跳过', { state: 'skipped', note: '这一轮没动过手(没写文件、没跑命令、没发消息),自动评审跳过了' }],
|
|
140
161
|
['首次告知', { state: 'notice', pendingKind: 'manual' }],
|
|
141
162
|
]
|
|
142
163
|
|
|
@@ -154,6 +175,7 @@ for (const [label, value] of cases) {
|
|
|
154
175
|
assert(fill.props.style.width === '51.11%', '细线宽度不对:' + fill.props.style.width + '(应为 23000/45000)')
|
|
155
176
|
assert(fill.props.style.background !== '#f59e0b', '还没到点就变琥珀了')
|
|
156
177
|
assert(text.indexOf('字幕最后一行在这里') >= 0, '字幕没露出来')
|
|
178
|
+
assert(text.indexOf('帮你理一理') >= 0, '碎碎念(思考中的那句)没画出来')
|
|
157
179
|
}
|
|
158
180
|
if (label === '评审中 到点') {
|
|
159
181
|
const fill = barOf(el)
|
|
@@ -178,7 +200,17 @@ for (const [label, value] of cases) {
|
|
|
178
200
|
assert(tiers.filter((t) => t === 'pri').length === 1, '未评这排应该正好一个主按钮,实际 ' + JSON.stringify(tiers))
|
|
179
201
|
assert(tiers.filter((t) => t === 'sec').length >= 1, '次按钮没画出来:' + JSON.stringify(tiers))
|
|
180
202
|
}
|
|
203
|
+
if (label === '完成 与主模型相同') {
|
|
204
|
+
assert(text.indexOf('与主模型相同') >= 0, '副模型和主模型是同一个时,卡片上没提示')
|
|
205
|
+
}
|
|
206
|
+
if (label === '闸门跳过') {
|
|
207
|
+
assert(text.indexOf('自动评审跳过') >= 0, '跳过了却没写出来')
|
|
208
|
+
assert(tiersOf(el).indexOf('pri') < 0, '跳过以后不该再摆一个主按钮(还没花钱,别催他)')
|
|
209
|
+
}
|
|
181
210
|
if (label === '完成 一剑封喉') {
|
|
211
|
+
assert(text.indexOf('复核 1.2k进') >= 0, '卡片末尾没显示复核的 token')
|
|
212
|
+
assert(text.indexOf('本轮 1.5k进') >= 0, '卡片末尾没显示这一轮的 token')
|
|
213
|
+
assert(text.indexOf('与主模型相同') < 0, '两模型不同却报了"与主模型相同"')
|
|
182
214
|
assert(text.indexOf('deepseek-flash') >= 0, '卡片上没有署名(模型名)')
|
|
183
215
|
assert(text.indexOf('署名') >= 0, '卡片上没写"署名"两个字')
|
|
184
216
|
assert(text.indexOf('引文核验') >= 0, '卡片上没有引文核验那一行')
|
package/test/core.test.mjs
CHANGED
|
@@ -25,7 +25,7 @@ fs.writeFileSync(path.join(ROOT, '记忆卡', '00-核心记忆卡.md'), '私人
|
|
|
25
25
|
fs.writeFileSync(path.join(ROOT, '普通文档.md'), '公开的')
|
|
26
26
|
|
|
27
27
|
const mod = await import(new URL('../index.js', import.meta.url).href)
|
|
28
|
-
const { parseReview, sanitizeReview, blockedPath, clipTranscript, transcriptOf, toolsFor, runTool, saveReview, shouldNudge, streamWithRetry, verifyUserQuotes, READ_TOOLS, WRITE_TOOL } = mod
|
|
28
|
+
const { parseReview, sanitizeReview, blockedPath, clipTranscript, transcriptOf, toolsFor, runTool, saveReview, shouldNudge, streamWithRetry, verifyUserQuotes, sameModel, turnOf, usageOfTurn, turnTouched, usageOfFinish, sumUsage, pickProModel, resolveReviewModel, READ_TOOLS, WRITE_TOOL } = mod
|
|
29
29
|
|
|
30
30
|
test.after(() => {
|
|
31
31
|
try {
|
|
@@ -764,3 +764,120 @@ test('落盘 md 带引文核验一节', () => {
|
|
|
764
764
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
765
765
|
}
|
|
766
766
|
})
|
|
767
|
+
|
|
768
|
+
/* ---------------- 1.3.5:主副模型是不是同一个 ---------------- */
|
|
769
|
+
|
|
770
|
+
test('同一个模型:忽略大小写和首尾空格', () => {
|
|
771
|
+
assert.equal(sameModel({ provider: 'deepseek-account', model: 'deepseek-flash' }, { provider: 'deepseek-account', model: ' DeepSeek-Flash ' }), true)
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
test('同一个模型名、provider 不同也算同一个(还是同一双眼睛)', () => {
|
|
775
|
+
assert.equal(sameModel({ provider: 'a', model: 'deepseek-flash' }, { provider: 'b', model: 'deepseek-flash' }), true)
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
test('不同模型不算;缺模型也不算(宁可不说,不许瞎说)', () => {
|
|
779
|
+
assert.equal(sameModel({ provider: 'a', model: 'deepseek-flash' }, { provider: 'a', model: 'deepseek-pro' }), false)
|
|
780
|
+
assert.equal(sameModel({ provider: 'a', model: '' }, { provider: 'a', model: '' }), false)
|
|
781
|
+
assert.equal(sameModel(null, { provider: 'a', model: 'deepseek-flash' }), false)
|
|
782
|
+
assert.equal(sameModel(undefined, undefined), false)
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
test('也吃字符串(别的地方可能只传模型名)', () => {
|
|
786
|
+
assert.equal(sameModel('deepseek-flash', { model: 'deepseek-flash' }), true)
|
|
787
|
+
assert.equal(sameModel('deepseek-flash', 'deepseek-pro'), false)
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
/* ---------------- 1.3.6:账从日志里算(不调模型)+自动评审闸门 ---------------- */
|
|
791
|
+
|
|
792
|
+
function fakeSession(extra = {}) {
|
|
793
|
+
return {
|
|
794
|
+
log: {
|
|
795
|
+
1: { type: 'user/message', seq: 1, data: { turn: 1 } },
|
|
796
|
+
2: { type: 'assistant/message', seq: 2, data: { turn: 1, usage: { inputTokens: 10, outputTokens: 5, cacheReadTokens: 3, totalTokens: 15 } } },
|
|
797
|
+
3: { type: 'tool/call', seq: 3, data: { turn: 1, name: 'read_file', arguments: '{}' } },
|
|
798
|
+
4: { type: 'assistant/message', seq: 4, data: { turn: 1, usage: { inputTokens: 20, outputTokens: 7, cacheReadTokens: 1, totalTokens: 27 } } },
|
|
799
|
+
...extra,
|
|
800
|
+
},
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
test('轮次:按被评的那条消息往前找 turn', () => {
|
|
805
|
+
assert.equal(turnOf(fakeSession(), 4), 1)
|
|
806
|
+
assert.equal(turnOf(fakeSession(), 2), 1)
|
|
807
|
+
assert.equal(turnOf({ log: {} }, 4), null)
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
test('这一轮主模型用了多少 token:按 turn 加总,不调模型', () => {
|
|
811
|
+
const u = usageOfTurn(fakeSession(), 4)
|
|
812
|
+
assert.equal(u.input, 30)
|
|
813
|
+
assert.equal(u.output, 12)
|
|
814
|
+
assert.equal(u.cacheRead, 4)
|
|
815
|
+
assert.equal(u.messages, 2)
|
|
816
|
+
assert.equal(usageOfTurn({ log: {} }, 4), null)
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
test('闸门:只读工具不算动手', () => {
|
|
820
|
+
assert.equal(turnTouched(fakeSession(), 4), false)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
test('闸门:写文件/跑命令/工作区变动都算动手', () => {
|
|
824
|
+
assert.equal(turnTouched(fakeSession({ 3: { type: 'tool/call', seq: 3, data: { turn: 1, name: 'write_file' } } }), 4), true)
|
|
825
|
+
assert.equal(turnTouched(fakeSession({ 3: { type: 'tool/call', seq: 3, data: { turn: 1, name: 'pwsh' } } }), 4), true)
|
|
826
|
+
assert.equal(turnTouched(fakeSession({ 5: { type: 'workspace/changes', seq: 5, data: { turn: 1 } } }), 4), true)
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
test('评审自己的用量:finish 里带就取,几种形状都认,没有就 null(不许估)', () => {
|
|
830
|
+
assert.deepEqual(usageOfFinish({ usage: { inputTokens: 100, outputTokens: 20, cacheReadTokens: 900 } }), {
|
|
831
|
+
input: 100,
|
|
832
|
+
output: 20,
|
|
833
|
+
cacheRead: 900,
|
|
834
|
+
cacheWrite: 0,
|
|
835
|
+
total: 120,
|
|
836
|
+
})
|
|
837
|
+
assert.equal(usageOfFinish({ message: { usage: { input_tokens: 5, output_tokens: 6 } } }).total, 11)
|
|
838
|
+
assert.equal(usageOfFinish({ kind: 'completed' }), null)
|
|
839
|
+
assert.equal(usageOfFinish(null), null)
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
test('多步评审的用量按次相加', () => {
|
|
843
|
+
const sum = sumUsage([
|
|
844
|
+
{ input: 100, output: 20, cacheRead: 0, cacheWrite: 0, total: 120 },
|
|
845
|
+
null,
|
|
846
|
+
{ input: 50, output: 5, cacheRead: 30, cacheWrite: 0, total: 55 },
|
|
847
|
+
])
|
|
848
|
+
assert.equal(sum.input, 150)
|
|
849
|
+
assert.equal(sum.output, 25)
|
|
850
|
+
assert.equal(sum.cacheRead, 30)
|
|
851
|
+
assert.equal(sumUsage([null, undefined]), null)
|
|
852
|
+
assert.equal(sumUsage([]), null)
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
/* ---------------- 1.3.6:默认副模型=自动 Pro 档 ---------------- */
|
|
856
|
+
|
|
857
|
+
test('从模型清单里挑 Pro 档', () => {
|
|
858
|
+
const list = [{ id: 'deepseek-flash' }, { id: 'deepseek-v4-pro', name: 'V4-Pro' }]
|
|
859
|
+
assert.equal(pickProModel(list), 'deepseek-v4-pro')
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
test('出了新一代 Pro,自动选新的(DSH 升级不用改设置)', () => {
|
|
863
|
+
assert.equal(pickProModel(['deepseek-v4-pro', 'deepseek-v4.1-pro']), 'deepseek-v4.1-pro')
|
|
864
|
+
assert.equal(pickProModel([{ id: 'deepseek-v5-pro' }, { id: 'deepseek-v4.1-pro' }]), 'deepseek-v5-pro')
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
test('清单里没有 Pro:不乱选一个便宜的充数', () => {
|
|
868
|
+
assert.equal(pickProModel(['deepseek-flash']), '')
|
|
869
|
+
assert.equal(pickProModel([]), '')
|
|
870
|
+
assert.equal(pickProModel(null), '')
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
test('手动指定模型时不去查清单、也不改人家的选择', async () => {
|
|
874
|
+
let called = 0
|
|
875
|
+
const llm = { listModels: async () => (called++, []) }
|
|
876
|
+
assert.equal(await resolveReviewModel(llm, { provider: 'x', model: 'my-model', modelAuto: false }), 'my-model')
|
|
877
|
+
assert.equal(called, 0)
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
test('自动档但查不到清单:退回配置里的 id,不报错', async () => {
|
|
881
|
+
const llm = { listModels: async () => { throw new Error('offline') } }
|
|
882
|
+
assert.equal(await resolveReviewModel(llm, { provider: 'x', model: 'deepseek-v4-pro', modelAuto: true }), 'deepseek-v4-pro')
|
|
883
|
+
})
|