@master0071/dsh-systemone 0.7.0 → 0.7.1
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 +2 -1
- package/lib/client.js +144 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -586,7 +586,7 @@ SystemOne 是「结构化问题 → 概率分布」的决策模型,**不是开
|
|
|
586
586
|
|
|
587
587
|
```powershell
|
|
588
588
|
npm run check # 9 个 lib/*.js 语法检查通过
|
|
589
|
-
npm run test #
|
|
589
|
+
npm run test # 198 项测试:
|
|
590
590
|
# - 场景库完整性、11 大场景端到端(有断言强制 sample 覆盖全部内置场景)、
|
|
591
591
|
# 自定义场景合并/覆盖/patch/非法跳过
|
|
592
592
|
# - params 覆盖语义、确定性、失败路径(无网络、无 Key)
|
|
@@ -596,6 +596,7 @@ npm run test # 195 项测试:
|
|
|
596
596
|
# 路由复用与热失效、失败熔断、卸载)
|
|
597
597
|
# - 用量台账(累计、跨日、重启后从文件恢复、坏行容错、日限判定)
|
|
598
598
|
# - 配置页(场景 JSON 校验、数字范围、保存被拒的冲突/校验区分、
|
|
599
|
+
# 三张卡各自保存(提交的 ops 只含本卡字段)、清除 Key 的 unset 语义、
|
|
599
600
|
# 加载态与"为什么不能保存"的渲染冒烟)
|
|
600
601
|
```
|
|
601
602
|
|
package/lib/client.js
CHANGED
|
@@ -532,6 +532,45 @@ window.__ModuleLoader__.load({
|
|
|
532
532
|
return latestRevision !== revision ? t('conflict') : t('saveRefused')
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
/**
|
|
536
|
+
* 构造一张卡片需要提交的写操作(纯函数)。
|
|
537
|
+
*
|
|
538
|
+
* 只取**该卡片内**、且与当前快照不同的字段——这是"三张卡各自保存"的核心:
|
|
539
|
+
* 保存「模型服务」时,不能顺手把「自动决策」卡里还没填完的草稿一起写下去。
|
|
540
|
+
*
|
|
541
|
+
* 规则与保存路径保持一致:数字从字符串转回 number(非法值不发写操作)、
|
|
542
|
+
* `redact` 下拉从字符串转布尔、`apiKey` 留空表示"不修改已保存的值"(清空走 unset)。
|
|
543
|
+
*
|
|
544
|
+
* @param {string} cardKey - service / custom / auto
|
|
545
|
+
* @param {object} input
|
|
546
|
+
* @param {string[]} input.fields - 该卡片的字段 key 列表
|
|
547
|
+
* @param {object} input.draft - 草稿(只含用户改过的键)
|
|
548
|
+
* @param {object} input.current - 当前快照(已 toDraft)
|
|
549
|
+
* @param {object[]} input.allFields - 全部字段元数据(用于判断类型)
|
|
550
|
+
* @param {boolean} input.clearApiKey - 是否点了「清除已保存的 Key」
|
|
551
|
+
* @returns {object[]} SettingsPathOpView 数组
|
|
552
|
+
*/
|
|
553
|
+
function cardOps(cardKey, { fields, draft, current, allFields, clearApiKey }) {
|
|
554
|
+
const ops = []
|
|
555
|
+
for (const [key, value] of Object.entries(draft ?? {})) {
|
|
556
|
+
if (!fields.includes(key)) continue
|
|
557
|
+
if (value === current[key]) continue
|
|
558
|
+
// apiKey 留空表示「保持已保存的值」,不发写操作(清空见下面的 unset)
|
|
559
|
+
if (key === 'apiKey' && value === '') continue
|
|
560
|
+
const field = allFields.find((item) => item.key === key)
|
|
561
|
+
let next = value
|
|
562
|
+
if (field?.kind === 'number') {
|
|
563
|
+
const parsed = Number(value)
|
|
564
|
+
if (!Number.isFinite(parsed)) continue
|
|
565
|
+
next = parsed
|
|
566
|
+
}
|
|
567
|
+
if (key === 'redact') next = value === 'true'
|
|
568
|
+
ops.push({ op: 'set', path: [key], value: next })
|
|
569
|
+
}
|
|
570
|
+
if (cardKey === 'service' && clearApiKey) ops.push({ op: 'unset', path: ['apiKey'] })
|
|
571
|
+
return ops
|
|
572
|
+
}
|
|
573
|
+
|
|
535
574
|
/* ────────────────────────────── 组件 ────────────────────────────── */
|
|
536
575
|
|
|
537
576
|
/**
|
|
@@ -750,69 +789,73 @@ window.__ModuleLoader__.load({
|
|
|
750
789
|
numberExtras[key] = { error }
|
|
751
790
|
}
|
|
752
791
|
|
|
753
|
-
const
|
|
754
|
-
const canSave = dirty && !busy && writable && customCheck.ok && Object.keys(numberErrors).length === 0
|
|
792
|
+
const changedKeys = Object.keys(draft ?? {}).filter((key) => draft[key] !== current[key])
|
|
755
793
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
for (const [key, error] of Object.entries(numberErrors)) blockers.push(`${t(key)}:${error}`)
|
|
762
|
-
const blockedText = blockers.length > 0
|
|
763
|
-
? t('saveBlocked', { count: blockers.length, reason: blockers[0] })
|
|
764
|
-
: undefined
|
|
794
|
+
const CARD_FIELDS = {
|
|
795
|
+
service: SERVICE_FIELDS.map((f) => f.key),
|
|
796
|
+
custom: CUSTOM_FIELDS.map((f) => f.key),
|
|
797
|
+
auto: AUTO_FIELDS.map((f) => f.key),
|
|
798
|
+
}
|
|
765
799
|
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
? [...new Set(customCheck.ids)]
|
|
771
|
-
.filter((id) => !SCENARIO_OPTIONS.some((option) => option.id === id))
|
|
772
|
-
.map((id) => ({ id, label: id }))
|
|
773
|
-
: []),
|
|
774
|
-
]
|
|
800
|
+
/** 单张卡片的脏态:该卡片内有字段被改过,或服务卡的 Key 被清空 */
|
|
801
|
+
const cardDirty = (cardKey) =>
|
|
802
|
+
(cardKey === 'service' && clearApiKey) ||
|
|
803
|
+
changedKeys.some((key) => CARD_FIELDS[cardKey].includes(key))
|
|
775
804
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (
|
|
780
|
-
|
|
805
|
+
/** 单张卡片的错误提示:该卡片内字段的数字越界,或自定义场景卡的 JSON 问题 */
|
|
806
|
+
const cardErrors = (cardKey) => {
|
|
807
|
+
const errors = []
|
|
808
|
+
if (cardKey === 'custom' && !customCheck.ok && customErrorText) {
|
|
809
|
+
errors.push(`${t('tabCustom')}:${customErrorText}`)
|
|
810
|
+
}
|
|
811
|
+
for (const [fieldKey, error] of Object.entries(numberErrors)) {
|
|
812
|
+
if (CARD_FIELDS[cardKey].includes(fieldKey)) errors.push(`${t(fieldKey)}:${error}`)
|
|
813
|
+
}
|
|
814
|
+
return errors
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** 单张卡片的保存状态行文案 */
|
|
818
|
+
const cardBlockedText = (cardKey) => {
|
|
819
|
+
const errors = cardErrors(cardKey)
|
|
820
|
+
return errors.length > 0 ? t('saveBlocked', { count: errors.length, reason: errors[0] }) : undefined
|
|
781
821
|
}
|
|
782
822
|
|
|
783
|
-
|
|
784
|
-
|
|
823
|
+
/** 单张卡片能否保存:本卡有改动、非忙、可写、且本卡内无校验错误 */
|
|
824
|
+
const cardCanSave = (cardKey) =>
|
|
825
|
+
cardDirty(cardKey) && !busy && writable && cardErrors(cardKey).length === 0
|
|
826
|
+
|
|
827
|
+
/** 把某张卡片的键从草稿里移除(撤销、或保存成功后);空草稿归一成 null */
|
|
828
|
+
const dropCardDraft = (cardKey) => {
|
|
829
|
+
setDraft((prev) => {
|
|
830
|
+
const next = { ...prev }
|
|
831
|
+
for (const key of Object.keys(prev)) {
|
|
832
|
+
if (CARD_FIELDS[cardKey].includes(key)) delete next[key]
|
|
833
|
+
}
|
|
834
|
+
return Object.keys(next).length ? next : null
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** 单张卡片保存:只提交该卡片内改过的字段,保存后只清掉该卡片的草稿 */
|
|
839
|
+
const onSaveCard = async (cardKey) => {
|
|
840
|
+
if (!cardCanSave(cardKey)) return
|
|
785
841
|
setBusy(true)
|
|
786
842
|
setNotice(null)
|
|
787
843
|
try {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
let next = value
|
|
796
|
-
if (field?.kind === 'number') {
|
|
797
|
-
const parsed = Number(value)
|
|
798
|
-
if (!Number.isFinite(parsed)) continue
|
|
799
|
-
next = parsed
|
|
800
|
-
}
|
|
801
|
-
// redact 下拉在草稿里是字符串,schema 要的是布尔
|
|
802
|
-
if (key === 'redact') next = value === 'true'
|
|
803
|
-
ops.push({ op: 'set', path: [key], value: next })
|
|
804
|
-
}
|
|
805
|
-
// 「清除已保存的 Key」是独立意图:schema 的 unset 才是"取消覆盖、回到默认",
|
|
806
|
-
// 用空字符串 set 只会写进一个空值。
|
|
807
|
-
if (clearApiKey) ops.push({ op: 'unset', path: ['apiKey'] })
|
|
844
|
+
const ops = cardOps(cardKey, {
|
|
845
|
+
fields: CARD_FIELDS[cardKey],
|
|
846
|
+
draft,
|
|
847
|
+
current,
|
|
848
|
+
allFields: ALL_FIELDS,
|
|
849
|
+
clearApiKey,
|
|
850
|
+
})
|
|
808
851
|
if (ops.length === 0) {
|
|
809
|
-
|
|
852
|
+
dropCardDraft(cardKey)
|
|
810
853
|
return
|
|
811
854
|
}
|
|
812
855
|
const ok = await form.mutate(ops, revision)
|
|
813
856
|
if (ok) {
|
|
814
|
-
|
|
815
|
-
setClearApiKey(false)
|
|
857
|
+
dropCardDraft(cardKey)
|
|
858
|
+
if (cardKey === 'service') setClearApiKey(false)
|
|
816
859
|
setNotice({ kind: 'ok', text: t('saved') })
|
|
817
860
|
} else {
|
|
818
861
|
setNotice({
|
|
@@ -827,6 +870,30 @@ window.__ModuleLoader__.load({
|
|
|
827
870
|
}
|
|
828
871
|
}
|
|
829
872
|
|
|
873
|
+
/** 单张卡片撤销:只撤销该卡片里的改动 */
|
|
874
|
+
const onRevertCard = (cardKey) => {
|
|
875
|
+
dropCardDraft(cardKey)
|
|
876
|
+
if (cardKey === 'service') setClearApiKey(false)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// 「固定场景」候选 = 11 个内置 + 当前生效的自定义场景 id(patch 条目指向已有 id,去重避免重复项)
|
|
880
|
+
const scenarioOptions = [
|
|
881
|
+
...SCENARIO_OPTIONS,
|
|
882
|
+
...(customCheck.ok
|
|
883
|
+
? [...new Set(customCheck.ids)]
|
|
884
|
+
.filter((id) => !SCENARIO_OPTIONS.some((option) => option.id === id))
|
|
885
|
+
.map((id) => ({ id, label: id }))
|
|
886
|
+
: []),
|
|
887
|
+
]
|
|
888
|
+
|
|
889
|
+
const onFieldChange = (key, value) => {
|
|
890
|
+
setNotice(null)
|
|
891
|
+
// 用户开始输入新 Key 时,「清除」这个意图就作废了——否则会先 set 再 unset。
|
|
892
|
+
if (key === 'apiKey' && clearApiKey) setClearApiKey(false)
|
|
893
|
+
setDraft((previous) => ({ ...(previous ?? {}), [key]: value }))
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
|
|
830
897
|
/** 渲染字段;可按 key 附加错误提示、候选、右上角附加元素。 */
|
|
831
898
|
const renderFields = (fields, extras = {}) =>
|
|
832
899
|
fields.map((field) => {
|
|
@@ -844,34 +911,41 @@ window.__ModuleLoader__.load({
|
|
|
844
911
|
})
|
|
845
912
|
})
|
|
846
913
|
|
|
847
|
-
|
|
914
|
+
// 每张卡片自己的 footer:保存只提交该卡片内改过的字段,撤销也只撤销该卡片
|
|
915
|
+
const renderFooter = ({ cardKey, showRevert, extra }) => hs('div', {
|
|
916
|
+
key: 'footer',
|
|
917
|
+
style: footerStyle,
|
|
848
918
|
children: [
|
|
849
|
-
h('
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
? h('span', { key: 'blocked', style: errorStyle, children: blockedText })
|
|
919
|
+
h('button', { key: 'save', type: 'button', style: { ...primaryButton, opacity: cardCanSave(cardKey) ? 1 : 0.5 }, disabled: !cardCanSave(cardKey), onClick: () => onSaveCard(cardKey), children: busy ? t('saving') : t('save') }),
|
|
920
|
+
...(showRevert ? [h('button', { key: 'revert', type: 'button', style: ghostButton, disabled: !cardDirty(cardKey) || busy, onClick: () => onRevertCard(cardKey), children: t('revert') })] : []),
|
|
921
|
+
...(extra ? [extra] : []),
|
|
922
|
+
cardBlockedText(cardKey) !== undefined
|
|
923
|
+
? h('span', { key: 'blocked', style: errorStyle, children: cardBlockedText(cardKey) })
|
|
855
924
|
: null,
|
|
856
|
-
!writable ? h('span', { key: 'ro', style: statusStyle, children: t('readOnly') }) : null,
|
|
857
925
|
],
|
|
858
926
|
})
|
|
859
927
|
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
928
|
+
// 全局状态(当前提供商 / 保存结果 / 只读)只显示一次;
|
|
929
|
+
// 每张卡自己的"为什么不能保存"由各自的 footer 渲染(见 renderFooter)。
|
|
930
|
+
const globalStatus = hs(Fragment, {
|
|
931
|
+
key: 'status',
|
|
864
932
|
children: [
|
|
865
|
-
h('
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
933
|
+
h('span', { key: 'now', children: t('current', { provider: values.provider || 'unisound', model: values.model || 'u2-decision' }) }),
|
|
934
|
+
notice !== null
|
|
935
|
+
? h('span', { key: 'notice', style: notice.kind === 'ok' ? statusStyle : errorStyle, children: notice.text })
|
|
936
|
+
: null,
|
|
937
|
+
!writable ? h('span', { key: 'ro', style: statusStyle, children: t('readOnly') }) : null,
|
|
869
938
|
],
|
|
870
939
|
})
|
|
871
940
|
|
|
872
941
|
return hs('ul', {
|
|
873
942
|
style: listStyle,
|
|
874
943
|
children: [
|
|
944
|
+
h('li', {
|
|
945
|
+
key: 'global-status',
|
|
946
|
+
style: { ...statusStyle, display: 'flex', gap: 10, flexWrap: 'wrap' },
|
|
947
|
+
children: globalStatus,
|
|
948
|
+
}),
|
|
875
949
|
h(ConfigCard, {
|
|
876
950
|
key: 'service',
|
|
877
951
|
title: t('tabService'),
|
|
@@ -898,7 +972,7 @@ window.__ModuleLoader__.load({
|
|
|
898
972
|
}),
|
|
899
973
|
},
|
|
900
974
|
}),
|
|
901
|
-
renderFooter({ showRevert: true }),
|
|
975
|
+
renderFooter({ cardKey: 'service', showRevert: true }),
|
|
902
976
|
],
|
|
903
977
|
}),
|
|
904
978
|
}),
|
|
@@ -961,6 +1035,7 @@ window.__ModuleLoader__.load({
|
|
|
961
1035
|
},
|
|
962
1036
|
}),
|
|
963
1037
|
renderFooter({
|
|
1038
|
+
cardKey: 'custom',
|
|
964
1039
|
showRevert: true,
|
|
965
1040
|
extra: customCheck.ok
|
|
966
1041
|
? h('span', {
|
|
@@ -984,7 +1059,7 @@ window.__ModuleLoader__.load({
|
|
|
984
1059
|
children: hs(Fragment, {
|
|
985
1060
|
children: [
|
|
986
1061
|
...renderFields(AUTO_FIELDS, numberExtras),
|
|
987
|
-
renderFooter({ showRevert: false }),
|
|
1062
|
+
renderFooter({ cardKey: 'auto', showRevert: false }),
|
|
988
1063
|
],
|
|
989
1064
|
}),
|
|
990
1065
|
}),
|
|
@@ -1036,6 +1111,7 @@ window.__ModuleLoader__.load({
|
|
|
1036
1111
|
describeCustomError,
|
|
1037
1112
|
numberErrorFor,
|
|
1038
1113
|
saveResultText,
|
|
1114
|
+
cardOps,
|
|
1039
1115
|
SERVICE_FIELDS,
|
|
1040
1116
|
CUSTOM_FIELDS,
|
|
1041
1117
|
AUTO_FIELDS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@master0071/dsh-systemone",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "DeepSeek Harness 插件:将 SystemOne 决策模型(默认云知声 u2-decision,可切换提供商)接入软件开发、需求预检、客服、内容审核、路由、销售线索、风控、招聘、数据治理、教育、需求变更等 11 大业务场景,为 Agent 返回带概率分布的决策与处置建议。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|