@leonhardthust/dsh-decision-engine 0.0.0-stage → 1.9.0
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/LICENSE +21 -0
- package/README.md +55 -2
- package/README.zh.md +57 -0
- package/config.example.yml +436 -0
- package/cordis.patch.yml +14 -0
- package/docs/assets/config-page.png +0 -0
- package/docs/assets/decisions-tab.png +0 -0
- package/docs/assets/hero.jpeg +0 -0
- package/docs/assets/stats-charts-preview.png +0 -0
- package/docs/assets/stats-donut-toggle.png +0 -0
- package/docs/assets/stats-interactive-charts.png +0 -0
- package/docs/assets/stats-point-donut.png +0 -0
- package/docs/assets/stats-ranking-bars.png +0 -0
- package/docs/lessons/cloudflare-builds-root-directory.md +51 -0
- package/docs/lessons/cold-start-timeout.md +134 -0
- package/docs/lessons/endpoint-path-vs-model.md +26 -0
- package/docs/lessons/model-field-required-by-provider.md +31 -0
- package/docs/local-dev-link.md +54 -0
- package/docs/releasing.md +67 -0
- package/docs/security-revision.md +130 -0
- package/icon.svg +6 -0
- package/lib/adapters/agent.js +314 -0
- package/lib/adapters/route.js +144 -0
- package/lib/adapters/tools.js +186 -0
- package/lib/adapters/translate.js +143 -0
- package/lib/approval-detail.js +325 -0
- package/lib/client.charts.js +36 -0
- package/lib/client.js +8589 -0
- package/lib/config-file.js +136 -0
- package/lib/credentials.js +187 -0
- package/lib/decision-cache.js +188 -0
- package/lib/decision-points.js +205 -0
- package/lib/decision-store.js +774 -0
- package/lib/endpoint-fallback.js +143 -0
- package/lib/engine.js +312 -0
- package/lib/guard/advisory.js +100 -0
- package/lib/guard/audit.js +79 -0
- package/lib/guard/context.js +128 -0
- package/lib/guard/effects.js +96 -0
- package/lib/guard/fallback.js +139 -0
- package/lib/guard/listener.js +191 -0
- package/lib/guard/policy.js +28 -0
- package/lib/home-paths.js +86 -0
- package/lib/index.js +3823 -0
- package/lib/outcomes.js +169 -0
- package/lib/points/spec.js +645 -0
- package/lib/providers.js +958 -0
- package/lib/resilience.js +308 -0
- package/lib/scope.js +68 -0
- package/lib/server-config.js +351 -0
- package/lib/session-events.js +75 -0
- package/lib/stats-store.js +916 -0
- package/lib/stats.js +1293 -0
- package/lib/telemetry.js +465 -0
- package/lib/tokenizer.js +201 -0
- package/lib/tolerance.js +498 -0
- package/lib/vendor/microcharts-charts.css +47 -0
- package/lib/vendor-src/entry.js +13 -0
- package/lib/vendor-src/jsx-shim.js +34 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +101 -3
- package/scripts/migrate-profile.mjs +102 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 同一个端点,浏览器 200 而 Node fetch 报错——不是「Node 不行」,是四类失败被
|
|
3
|
+
* 混成了同一个「报错」。
|
|
4
|
+
*
|
|
5
|
+
* 用户原话:
|
|
6
|
+
*
|
|
7
|
+
* 「官方文档页点『发送请求』永远返回 200,在插件里配好同一 provider 点
|
|
8
|
+
* 『测试』大约 2/3 会报错。」
|
|
9
|
+
*
|
|
10
|
+
* 2026-11 实测(bamboonode `https://api.bamboonode.dev/v1/decisions`,
|
|
11
|
+
* 模型 startlux-decision-v1.1-27b-autothink,生产遥测 2573 次请求 / 358 次降级):
|
|
12
|
+
*
|
|
13
|
+
* 场景 实测延迟
|
|
14
|
+
* ──────────────────────────────── ────────────────────────────────
|
|
15
|
+
* 闲置 110 秒后首发 7.4–15.9 s(服务端冷启动尾延迟)
|
|
16
|
+
* 焐热之后 1.0–1.3 s
|
|
17
|
+
* 首发被掐断后的第二发 1.1–1.7 s,4 组独立实验一致
|
|
18
|
+
* thinkMode: never / auto 约 1.9 s / 约 7.2 s
|
|
19
|
+
*
|
|
20
|
+
* 成功请求延迟分布(n=2215):
|
|
21
|
+
* <1s 36.1% | 1-2s 31.8% | 2-4s 18.3% | 4-6s 8.8% | 6-8s 5.0% | ≥8s 0%
|
|
22
|
+
*
|
|
23
|
+
* 注意最后那个 0%:**不是没有慢请求,是慢的那些全被 8 秒预算掐掉了**,
|
|
24
|
+
* 根本没资格算进「成功」。只看成功样本会得出「这个端点从不超 8 秒」的错觉。
|
|
25
|
+
*
|
|
26
|
+
* 四类失败:判别特征与修复动作完全不同
|
|
27
|
+
* ---------------------------------------------------------------------------
|
|
28
|
+
* 类别 判别特征 修复动作
|
|
29
|
+
* ──────── ────────────────────────────────── ──────────────────
|
|
30
|
+
* 超时 耗时卡在预算线整数值 抬 timeoutMs;
|
|
31
|
+
* (如 8000 / 8001 / 8002 / 8003, 冷启动首发值得重试
|
|
32
|
+
* 本机实测 123 次全聚在 7999–8005)
|
|
33
|
+
* 网络错误 错误文本 `fetch failed`, 换链路 / 查代理;
|
|
34
|
+
* 耗时 5 秒上下(实测 5004–5557 ms) 重试同样有效
|
|
35
|
+
* 5xx 带服务商自己的 JSON 错误体: 本地改不了;
|
|
36
|
+
* `{"error":"Task request failed"}` 降级放行即可
|
|
37
|
+
* `{"error":{"message":"Database
|
|
38
|
+
* error, please contact the
|
|
39
|
+
* administrator (request id: …)",
|
|
40
|
+
* "type":"new_api_error"}}`
|
|
41
|
+
* 冷却 耗时 0–1 ms 等冷却结束;
|
|
42
|
+
* (本机 113 次:0 ms×106、1 ms×7) 半开试探可消除
|
|
43
|
+
* 请求根本没发出去
|
|
44
|
+
*
|
|
45
|
+
* 这四类混成一句「测试失败」,用户唯一能做的动作就是反复点「测试」——
|
|
46
|
+
* 而反复点恰好每次都采在最差点上(见下)。所以判别的第一步永远是**看耗时**:
|
|
47
|
+
* 0 ms 说明请求没发出去,8 秒整数说明被自己的预算掐了,5 秒上下且
|
|
48
|
+
* `fetch failed` 说明链路层就没通,带 request id 的 JSON 才是服务端自己的事。
|
|
49
|
+
*
|
|
50
|
+
* 为什么「点测试」永远看到最坏情况
|
|
51
|
+
* ---------------------------------------------------------------------------
|
|
52
|
+
*
|
|
53
|
+
* 「测试」是手动触发的,而人点它的时机是「刚想起来 / 刚改完配置」——也就是
|
|
54
|
+
* 端点**闲置了一段时间**之后。每一次点击都是一次冷启动首发,等于永远在延迟
|
|
55
|
+
* 分布的最右端采样。真实降级率是 **13.9%**(2573 次请求 / 358 次降级),
|
|
56
|
+
* 不是 2/3;2/3 是这个采样偏差的产物,不是端点的属性。
|
|
57
|
+
*
|
|
58
|
+
* 358 次降级的构成:timeout 83 | network-error 84 | http-5xx 95 | cooldown 93
|
|
59
|
+
* | caller-aborted 2 | 429 1 | 502 1 | 530 2。
|
|
60
|
+
*
|
|
61
|
+
* 顺带一个放大器:连续 3 次失败进入 60 秒冷却,而一次抖动往往 3 秒就过去了。
|
|
62
|
+
* 于是「3 秒的服务端抖动」被放大成「60 秒完全不工作」,冷却期间的请求以
|
|
63
|
+
* 0 ms 计入降级——它把一次小故障记成了一串大故障。
|
|
64
|
+
*
|
|
65
|
+
* 代理:能解释一部分网络错误,但不是可靠修复
|
|
66
|
+
* ---------------------------------------------------------------------------
|
|
67
|
+
*
|
|
68
|
+
* 浏览器那一侧为什么看起来更皮实:文档页的按钮**没有短超时**(实测 15.9 秒
|
|
69
|
+
* 也照等),并且走系统代理(Clash 127.0.0.1:7897),连接重置会被透明重试。
|
|
70
|
+
* 插件是 Node fetch、直连、POST 不重试、外加 8 秒硬预算——三条差异叠起来,
|
|
71
|
+
* 同一个端点在两边表现完全不同。
|
|
72
|
+
*
|
|
73
|
+
* 关于代理,有三条容易记反的事实:
|
|
74
|
+
*
|
|
75
|
+
* 1. **给 Node fetch 设 `HTTPS_PROXY` 环境变量是无效的**。Node 的 fetch
|
|
76
|
+
* 不读它。实测反而变成 `UND_ERR_CONNECT_TIMEOUT`(10.5 秒)+ 连续
|
|
77
|
+
* ECONNRESET——比不设还差。
|
|
78
|
+
* 2. **Node 26 起 `NODE_USE_ENV_PROXY=1`(或 `--use-env-proxy`)才真正生效**。
|
|
79
|
+
* 实测某次 4001 ms → 1949 ms,确实是改善。
|
|
80
|
+
* 3. **但在冷启动场景代理并不稳定优于直连**:同一次实验里 8030 ms(代理)
|
|
81
|
+
* vs 6035 ms(直连)。所以代理是**环境相关的诊断手段,不是可靠修复**。
|
|
82
|
+
* 本机 DNS 解析到 198.18.0.145(Clash fake-IP),这本身就说明链路被
|
|
83
|
+
* 本机代理接管过——排查时先确认这一层在做什么,再谈插件的问题。
|
|
84
|
+
*
|
|
85
|
+
* 可靠修复是**重试**:首发被掐断时服务端其实已经被焐热了,第二发 1.1–1.7 秒
|
|
86
|
+
* 必中(4 组独立实验一致)。只对 timeout / network-error 重试,4xx / 5xx
|
|
87
|
+
* 不重试——配置错和服务端故障重试只是多烧一次额度、拿回同一个错误。
|
|
88
|
+
*
|
|
89
|
+
* 一条可直接跑的自查命令
|
|
90
|
+
* ---------------------------------------------------------------------------
|
|
91
|
+
*
|
|
92
|
+
* 失败记录的特征是 **`action: "degraded"`**,不是某个布尔字段(明细文件里的
|
|
93
|
+
* `degraded` 是另一套口径,别拿它过滤遥测)。先按 detail 分类计数:
|
|
94
|
+
*
|
|
95
|
+
* jq -r 'select(.action=="degraded") | .detail' \
|
|
96
|
+
* ~/.dsh/dsh-decision-engine/telemetry.jsonl | sort | uniq -c | sort -rn
|
|
97
|
+
*
|
|
98
|
+
* 再加一列耗时,四类失败立刻分开(同一条记录里的 `latencyMs` 就是判别特征):
|
|
99
|
+
*
|
|
100
|
+
* jq -r 'select(.action=="degraded") | "\(.detail)\t\(.latencyMs // 0)"' \
|
|
101
|
+
* ~/.dsh/dsh-decision-engine/telemetry.jsonl \
|
|
102
|
+
* | awk -F'\t' '{c[$1]++; s[$1]+=$2;
|
|
103
|
+
* if(!($1 in mn)||$2<mn[$1])mn[$1]=$2;
|
|
104
|
+
* if($2>mx[$1])mx[$1]=$2}
|
|
105
|
+
* END{for(k in c) printf "%-16s %5d 次 耗时 %6d-%-7d ms\n", k, c[k], mn[k], mx[k]}' \
|
|
106
|
+
* | sort -k2 -rn
|
|
107
|
+
*
|
|
108
|
+
* 实跑输出(本机 telemetry.jsonl,窗口与上文的 2573 次不同,仅示范判读):
|
|
109
|
+
*
|
|
110
|
+
* timeout 123 次 耗时 7999-1007518 ms
|
|
111
|
+
* network-error 115 次 耗时 1076-7969 ms
|
|
112
|
+
* cooldown 113 次 耗时 0-1 ms
|
|
113
|
+
* http-500 109 次 耗时 258-7560 ms
|
|
114
|
+
* no-key 48 次 耗时 0-3 ms
|
|
115
|
+
* http-530 2 次 耗时 2316-4295 ms
|
|
116
|
+
* http-503 2 次 耗时 84-245 ms
|
|
117
|
+
* caller-aborted 2 次 耗时 453-1865 ms
|
|
118
|
+
* http-502 1 次 耗时 1257-1257 ms
|
|
119
|
+
* http-429 1 次 耗时 2883-2883 ms
|
|
120
|
+
* http-400 1 次 耗时 62-62 ms
|
|
121
|
+
*
|
|
122
|
+
* 判读:timeout 的 7999–8005 是 8 秒预算的形状(两个 >10s 的离群值是进程被
|
|
123
|
+
* 挂起,不是网络);cooldown 的 0–1 ms 证明请求没发出去;network-error 全部
|
|
124
|
+
* 是 `fetch failed` 且聚在 5 秒上下,那是链路层在超时而不是服务端在回话。
|
|
125
|
+
* 想进一步分 5xx 到底是谁的问题:
|
|
126
|
+
*
|
|
127
|
+
* jq -r 'select(.action=="degraded" and (.detail|startswith("http-5"))) | .extra.body' \
|
|
128
|
+
* ~/.dsh/dsh-decision-engine/telemetry.jsonl | sort | uniq -c | sort -rn
|
|
129
|
+
*
|
|
130
|
+
* 带 request id 的 `new_api_error` 是服务商内部的数据库错误——本地无论怎么
|
|
131
|
+
* 改配置都不会变好,只能等或换 provider。
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 端点路径(/decisions 还是 /systemone)与模型名无关,只与**服务商**有关。
|
|
3
|
+
*
|
|
4
|
+
* 2026-10-25 实测(真实 key,打真实端点):
|
|
5
|
+
*
|
|
6
|
+
* 服务商 模型 /v1/decisions /v1/systemone
|
|
7
|
+
* ──────────────── ──────────────────────────── ────────────────── ──────────────────
|
|
8
|
+
* SiliconFlow Kev-4B 404 Not Found 200 ✅
|
|
9
|
+
* bamboonode startlux-decision-v1.1-27b-autothink 200 ✅ 200 ✅
|
|
10
|
+
* bamboonode typesafe/jev-1.14 400 not served 400 not served
|
|
11
|
+
*
|
|
12
|
+
* 结论:
|
|
13
|
+
*
|
|
14
|
+
* 1. **路径由服务商决定,不由模型决定**。bamboonode 的决策模型在两条路径上
|
|
15
|
+
* 都能用;SiliconFlow 的 Kev-4B 只认 /systemone。
|
|
16
|
+
* 2. **404 = 路径不存在**,400 = 请求内容/模型不合法。这是能安全用来
|
|
17
|
+
* 「换一条路径重试」的信号(实测两类网关都如此)。
|
|
18
|
+
* 3. 同一个 key 在 SiliconFlow 上打 /v1/decisions 得到的是纯文本
|
|
19
|
+
* `Not Found`——不是 JSON 错误体——因此降级时必须报出真实端点,
|
|
20
|
+
* 否则用户看到「HTTP 404」完全猜不到是自己路径选错了。
|
|
21
|
+
*
|
|
22
|
+
* 现场影响(telemetry.jsonl,2026-10-25 13:16–13:41):8 次 `http-404` 降级。
|
|
23
|
+
* 守卫是 fail-open 的,所以它**静默地什么也没做**——用户只看到「守卫没反应」。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「模型留空」的含义由服务商决定,不是本插件能统一的。
|
|
3
|
+
*
|
|
4
|
+
* 2026-10-25 实测(真实 key,打真实端点):
|
|
5
|
+
*
|
|
6
|
+
* 服务商 请求 结果
|
|
7
|
+
* ────────────── ──────────────────────────── ──────────────────────────
|
|
8
|
+
* SiliconFlow 省略 model 400 Model field is required.
|
|
9
|
+
* SiliconFlow model: "" 400 Model field is required.
|
|
10
|
+
* SiliconFlow model: Kev-4B 200 ✅
|
|
11
|
+
* bamboonode 省略 model 200 ✅(服务端回填默认模型)
|
|
12
|
+
*
|
|
13
|
+
* 结论:
|
|
14
|
+
*
|
|
15
|
+
* 1. **model 是否必填由服务商决定**。SiliconFlow 的 /systemone 把 model 列为
|
|
16
|
+
* required(官方文档也这么写),而 bamboonode 省略字段会由服务端回填。
|
|
17
|
+
* 2. 因此「留空 = 不发 model 字段」这条规则只在**服务端接受省略**时成立。
|
|
18
|
+
* 套到必填端点上就是一次注定的 400,而守卫是 fail-open 的——表现为
|
|
19
|
+
* **静默失效**:用户只看到「守卫没反应」,日志里是 http-400 降级。
|
|
20
|
+
* 3. 反过来把模型名钉死在配置里也不好:服务商给模型改名时配置就失效了。
|
|
21
|
+
* 正确做法是**预设带默认模型、配置留空即用默认、填了以填的为准**
|
|
22
|
+
* (见 providers.js 的 `requiresModel` / `defaultModel`)。
|
|
23
|
+
*
|
|
24
|
+
* 附带一条同源的坑:/v1/models 不能直接当决策模型清单用。SiliconFlow 会返回
|
|
25
|
+
* 98 个模型、且**没有一条**带 `supported_endpoint_types`,而 /systemone 只认
|
|
26
|
+
* Kev-4B / SemIf——chat 模型打过去是 400 `Model does not exist.`。把它们都塞进
|
|
27
|
+
* 下拉,就是「96 个选了就失败的选项」。这类端点只能按预设的 `knownModels`
|
|
28
|
+
* 白名单筛。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# 本地开发:把仓库链进 DSH profile
|
|
2
|
+
|
|
3
|
+
改代码 → 重启 DSH → 生效。
|
|
4
|
+
|
|
5
|
+
## 做法
|
|
6
|
+
|
|
7
|
+
profile 的 `node_modules/dsh-decision-engine` 是**软链**指向本仓库:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
~/.dsh/profiles/desktop/node_modules/dsh-decision-engine -> /Users/chi/Code/dsh-decision-engine
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
profile 的 `package.json` 里对应声明必须是 `link:` 形式:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
"dsh-decision-engine": "link:/Users/chi/Code/dsh-decision-engine"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**为什么不能只是手动建软链**:profile 用 pnpm 管理(`nodeLinker: hoisted`),
|
|
20
|
+
而 `package.json` 里若仍写 `"0.6.0"`,下一次 `pnpm install` 会照 lockfile
|
|
21
|
+
把 registry 版本装回去,**软链被覆盖**。改成 `link:` 后 pnpm 认得它是
|
|
22
|
+
本地链接,重装也保留。
|
|
23
|
+
|
|
24
|
+
已验证:`pnpm install` 之后软链仍是符号链接。
|
|
25
|
+
|
|
26
|
+
## 回退到 registry 版本
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
cd ~/.dsh/profiles/desktop
|
|
30
|
+
# 1. 改回版本号
|
|
31
|
+
# "dsh-decision-engine": "0.6.0"
|
|
32
|
+
# 2. 重装
|
|
33
|
+
pnpm install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 注意
|
|
37
|
+
|
|
38
|
+
- **改配置是热加载**(`config.yml` 改了立刻生效,不用重启)。
|
|
39
|
+
- **改代码必须重启 DSH**:插件在启动时加载。
|
|
40
|
+
- `pnpm install` 第一次把 `link:` 接上时,会把仓库 `package.json` 的版本号
|
|
41
|
+
从 `0.6.0` 改写成 `0.6.1`(pnpm 的行为)。这是**一次性**的,之后
|
|
42
|
+
`Already up to date` 不再动它。版本号被改了直接 `git checkout package.json` 还原。
|
|
43
|
+
|
|
44
|
+
## 测试
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
cd /Users/chi/Code/dsh-decision-engine
|
|
48
|
+
node --test test/ # 190 项
|
|
49
|
+
node scripts/verify-wiring.mjs
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> Node 24(DSH 自带 runtime)不支持 `node --test test/` 这种目录参数,
|
|
53
|
+
> 会报 `Cannot find module '.../test'`。用显式 glob 即可:
|
|
54
|
+
> `node --test "test/*.test.js"`。Node 26(homebrew)两种写法都行。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# npm 发布
|
|
2
|
+
|
|
3
|
+
发布目标:`@leonhardthust/dsh-decision-engine`,当前版本 `1.9.0`,公开包。
|
|
4
|
+
|
|
5
|
+
本分支基于 `zuosus/dsh-decision-engine` 的 MIT 代码,保留原作者与许可证;npm scope、源码仓库、bundle 引用和浏览器主模块 / charts chunk 标识已同步为本分支身份。内部 API、样式标识和用户数据目录保留旧名称以兼容已有配置。不要同时启用原包与本 scoped fork,它们提供相同的服务与页面。
|
|
6
|
+
|
|
7
|
+
## 工作流
|
|
8
|
+
|
|
9
|
+
- `.github/workflows/ci.yml`:main push、PR、手动运行或发布工作流复用;Ubuntu 24.04 + Node.js 24 + Vite+ 1.6.0。执行 frozen install、完整测试与接线、隔离情景、实际打包和 tarball 检查。失败不会被忽略。
|
|
10
|
+
- `.github/workflows/publish.yml`:版本 tag push 或 main 上手动运行。只有 `LeonhardtHUST/dsh-decision-engine` 可以进入发布;发布必须等待上述 CI 成功。
|
|
11
|
+
- 手动运行必须填写与 manifest 完全一致的 `v1.9.0`。后续发版同步修改 manifest 与 tag。预发布版本不能覆盖 npm `latest`。
|
|
12
|
+
- checkout 与 setup-vp Action 固定到已核对的 commit SHA;不通过 `pull_request_target` 运行不可信 PR。
|
|
13
|
+
- 验证任务只有 `contents: read`;只有发布 job 有 `id-token: write`。代码 checkout 不保留 Git 凭据。此工作流不会自动修改版本、提交源码或推送 Git。
|
|
14
|
+
|
|
15
|
+
## npm 授权
|
|
16
|
+
|
|
17
|
+
首次发包通常需要先用有 `@leonhardthust` scope 发布权限的账号登录:
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
$env:HTTPS_PROXY = "http://localhost:7897"
|
|
21
|
+
$env:HTTP_PROXY = "http://localhost:7897"
|
|
22
|
+
npm config get registry --registry "https://registry.npmjs.org/"
|
|
23
|
+
npm login --registry "https://registry.npmjs.org/" --auth-type web
|
|
24
|
+
npm whoami --registry "https://registry.npmjs.org/"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
认证直接调用原生 npm,不经过当前 `vp pm login` 的参数透传;已观察到该透传把 registry 错误解析成 `https` 后回落到镜像。仓库依赖与验证任务继续由 vp 管理。CLI 必须显示 npmjs 官方 registry;出现 npmmirror 等镜像时立即取消。代理环境变量只影响这个终端。不要把 npm token 贴到聊天、提交到源码或打印到 CI 日志。网页登录和二次验证由用户完成。
|
|
28
|
+
|
|
29
|
+
CI 发布支持两种授权:
|
|
30
|
+
|
|
31
|
+
1. **npm Trusted Publishing(优先)**:在 npm 包设置登记 GitHub owner=`LeonhardtHUST`、repo=`dsh-decision-engine`、workflow=`publish.yml`。工作流用 OIDC 和支持 trusted publishing 的 npm CLI 发布,并生成 provenance;不必保存长期 token。首次创建 scoped 包后再配置可信发布者。
|
|
32
|
+
2. **已有发布凭据**:仓库 Actions secret `NPM_TOKEN`。工作流仅在最后的授权与发布步骤提供它,临时 npmrc 使用环境变量引用,不把 token 插进文件内容。token 权限与 npm 的 2FA/发布策略必须允许该包发布;未授权会明确失败,不静默换身份。
|
|
33
|
+
|
|
34
|
+
不要把仅有 GitHub 仓库 ADMIN 权限理解成拥有 npm scope 权限。
|
|
35
|
+
|
|
36
|
+
## 发布前检查
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
vp install --frozen-lockfile
|
|
40
|
+
vp run verify
|
|
41
|
+
vp run verify:guard
|
|
42
|
+
vp run pack:release
|
|
43
|
+
vp run verify:package
|
|
44
|
+
$env:RELEASE_TAG = "v1.9.0"
|
|
45
|
+
vp run verify:release
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`release-artifacts/` 必须只有本次发布的一个 `.tgz`;重复打包前手动核对其中内容,勿混入旧版 tarball。目录和 tarball 被 Git 忽略。
|
|
49
|
+
|
|
50
|
+
`verify:package` 读取真正的 gzip/tar,检查 manifest、scoped 模块 id、bundle 引用、全部安全模块、locale、图表资源和相对 import。配置、凭据、日志、测试目录等不能进入包。输出文件数、大小和 SHA-512 integrity。
|
|
51
|
+
|
|
52
|
+
当前 Windows 完整测试的 5 个基线失败及安全边界见 `bench/guard-revision/validation.md`(仅在源码仓库保留完整证据)。发布 CI 选择 Linux,依然必须运行完整测试,不跳过或忽略它们;本机专测通过不等同于 CI 发布成功。
|
|
53
|
+
|
|
54
|
+
## 手动发布相同的已验证 tarball
|
|
55
|
+
|
|
56
|
+
授权及完整验证均满足后:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
npm publish ./release-artifacts/leonhardthust-dsh-decision-engine-1.9.0.tgz --registry "https://registry.npmjs.org/" --access public --tag latest
|
|
60
|
+
npm view @leonhardthust/dsh-decision-engine version --registry "https://registry.npmjs.org/" --json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
本地发布不伪造 GitHub provenance;在 GitHub Actions 中由工作流增加 `--provenance`。发布操作不可覆盖已存在的同版本;版本冲突必须停止,不自动递增重试。
|
|
64
|
+
|
|
65
|
+
## 安装
|
|
66
|
+
|
|
67
|
+
DSH 插件管理中使用 `@leonhardthust/dsh-decision-engine` 或指定 `@leonhardthust/dsh-decision-engine@1.9.0`。安装和启用是另外的用户动作,发布工作流不会修改本机 DSH profile。
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# 设置与授权安全修订
|
|
2
|
+
|
|
3
|
+
## 范围与基线
|
|
4
|
+
|
|
5
|
+
- 基线:`ff758465166184455d566f851841308bdc682fb4`,工作区开始时干净。
|
|
6
|
+
- 对齐用户提供的 captain task 与 revision blueprint,保留原始 `decideGuardLegacy` 作离线反事实对照。
|
|
7
|
+
- 用户随后要求直接在当前仓库按阶段自动提交,因此没有创建额外分支、没有重置基线、没有推送远端。
|
|
8
|
+
- 不修改本机全局 DSH 配置、不安装到当前运行的 profile、不在生产会话执行高危验证。
|
|
9
|
+
- 仓库通过 `vp install` / `vp run test` 管理,保留 Node.js 原生测试框架,不把测试迁移成 Vitest。
|
|
10
|
+
- 提交校验继承 `@commitlint/config-angular`,额外要求 subject 包含汉字;通过 Vite+ commit-msg hook 执行。
|
|
11
|
+
|
|
12
|
+
## 设置变化
|
|
13
|
+
|
|
14
|
+
主开关控制整个决策引擎;工具授权只有一组关闭 / 观察 / 生效控件,同时同步旧的 `mode` 和 `guardEnabled`,不再与参与点清单重复。
|
|
15
|
+
|
|
16
|
+
- 关闭:该决策点不参与,不发送决策请求,也不提供插件安全拦截。
|
|
17
|
+
- 观察:记录候选结果,不改变宿主处置,不能宣称已经保护操作。
|
|
18
|
+
- 生效:先实施 mandatory,再使用模型建议处理剩余软审查。
|
|
19
|
+
- 其他决策点保持独立设置。
|
|
20
|
+
- 决策模型和回退 provider / model / reasoning effort 均支持输入;宿主模型库可用时提供列表。
|
|
21
|
+
- 模型库通过 `GET /api/decision-engine/guard-models` 发现;查询参数 `provider` / `model` 用于按需加载 reasoning effort 元数据。不返回密钥。
|
|
22
|
+
- 设置页和决策历史卡片页的字体顺序均为 Sarasa Gothic SC,再系统默认无衬线字体。决策历史表格、展开明细、空态和错误状态一并提升字号,中文可读内容不再落入纯等宽字体链。保留表头粘性、排序、折叠与明细滚动行为。
|
|
23
|
+
- 高级设置折叠,保留用户分组展开状态。
|
|
24
|
+
|
|
25
|
+
## 分层结构
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
ToolExecution
|
|
29
|
+
→ TrustedContext(宿主来源、真实工具可见性、权限与接收方核验)
|
|
30
|
+
→ ToolEffect(本地读取 / 有限写入 / 内部消息 / 状态 / 网络 / 提权 / 破坏 / 未知)
|
|
31
|
+
→ MandatoryPolicy(safe / requires_approval / deny / unknown)
|
|
32
|
+
→ ModelAdvisory(low / medium / high、注入、不可逆、任务相关性)
|
|
33
|
+
→ Tolerance(只调整软审核摩擦)
|
|
34
|
+
→ Auto Fallback(仅限软 ask 或建议失败)
|
|
35
|
+
→ PreToolDecision + 单调 tools.guard
|
|
36
|
+
→ tools/result(reported-success / reported-error / unknown)
|
|
37
|
+
→ 摘要审计(相同 session_id + call_id 汇合)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 信任字段与接口缺口
|
|
41
|
+
|
|
42
|
+
核对对象为安装中的 DSH `0.2.2-rc.2` 的 Inspect 契约;审批事件结构另读取 `@deepseek-ai/dsh-user-approval` 实现核对。
|
|
43
|
+
|
|
44
|
+
| 字段 | 来源 | 信任与限制 |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| 会话 / 调用 / 执行 token | `ToolExecution`、Agent Session | 宿主结构化身份,不能由 args 声称 |
|
|
47
|
+
| 原始真人目标 | `session.snapshotEvents()`,仅 `user/message` 且 `source.kind=user` | 没有就 unknown;recent 不作为授权证据 |
|
|
48
|
+
| 会话审批 override | `approval.overrideOf(session)` | 只返回 override,未声明不等于批准 |
|
|
49
|
+
| 预设与缺省审批 | `permissionPresets.current/resolve` | 用于补足当前 bundle;custom 或不可用保持 unknown |
|
|
50
|
+
| 沙箱与可写根 | `sandboxPolicy.resolve({session})` | 能读到约束不等于已确认底层执行器强制实施 |
|
|
51
|
+
| 文件目标范围 | `fs.resolve` + `fs.contains` | 写前解析快照;不宣称消除了 TOCTOU,也不授予工作区外写权限 |
|
|
52
|
+
| 工具可执行性 | `tools.executable(name, agent, toolSet)` | 读到 unavailable 时拒绝;服务缺失保持 unknown |
|
|
53
|
+
| 内部接收方 | root / descendants + `agents.get` | 要求同树、可继续且本地存活;不继承发送方授权 |
|
|
54
|
+
| 下游独立审核 | 本插件全局 enforce 注册事实 | 仅对经过当前 Host 工具管线的本地 continuable Agent 成立,外部进程未知 |
|
|
55
|
+
| 接收方具体权限 / delegated scope | 无完整授权接口 | unknown,不凭 reviewer 等名字推断 |
|
|
56
|
+
| 本次真人批准 | 宿主 `approval/asked` → `approval/decided` | 必须在本次审查之后,匹配工具名、call_id、请求 id,且 outcome=allowed-once |
|
|
57
|
+
| 宿主最终动作 | 未获得独立完整的最终处置接口 | unknown;不把 next() 写成最终允许 |
|
|
58
|
+
| 实际副作用 | `tools/result` | 只记录 reported-success/error,不能证明磁盘、网络或外部系统副作用完成 |
|
|
59
|
+
|
|
60
|
+
### 安全分级与不变量
|
|
61
|
+
|
|
62
|
+
安全分级不再等于一个模型风险分数。
|
|
63
|
+
|
|
64
|
+
1. **safe**:已知无副作用本地查询、受宿主修订协议约束的任务状态操作、经过独立核验的内部消息;有限写入仅在宿主目标解析与允许范围内进入模型审查。
|
|
65
|
+
2. **requires_approval**:越界写入、只读环境写入、权限提升、可能破坏、未知高影响执行、未知外部能力、接收方授权未核验。
|
|
66
|
+
3. **deny**:宿主已明确拒绝工具可见性;所有档位保持拒绝。
|
|
67
|
+
4. **unknown**:缺失的来源与权限不能默认为授权;高影响 unknown 升级 requires_approval,不能靠模型低风险放行。
|
|
68
|
+
|
|
69
|
+
`danger-full-access` 只表示沙箱约束,不是所有操作的授权。`approval=never` 下 required 调用直接拒绝。审批可用也不是批准:最终单调 guard 要求本次 allowed-once,并核对被审查参数摘要与调用身份,防止其他 waterfall 把 ask 改为 allow 或修改参数。
|
|
70
|
+
|
|
71
|
+
严格安全白名单不包括任意 `web_fetch`、MCP、shell 或补丁;`todo_write` / `team_task_update` 是有限状态修改,不伪装成只读。无法可靠解析 shell 时不通过正则“证明安全”;危险关键词只帮助说明原因,没命中仍是未知高影响。这会提高无人审批环境下任意 shell/MCP 的拒绝率,是当前宿主缺少精细预授权接口的明确安全退化。
|
|
72
|
+
|
|
73
|
+
### 自动审查与 Fallback
|
|
74
|
+
|
|
75
|
+
宿主 `auto` 不再提前跳过整个授权审查。已验证的本地查询可不联系模型;其他可审查调用继续先问决策模型。
|
|
76
|
+
|
|
77
|
+
`auto` 或 `tolerance=autonomous` 遇到软性 ask,或模型建议不可用时,使用以下策略:
|
|
78
|
+
|
|
79
|
+
| `guardFallback` | 行为 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `model` | 指定宿主已注册 provider / model,可设置 reasoning effort |
|
|
82
|
+
| `session` | 沿用当前 request/header 的 provider / model / reasoning effort(默认) |
|
|
83
|
+
| `allow` | 跳过软审核建议,仍不覆盖 mandatory |
|
|
84
|
+
| `deny` | 拒绝该软审核请求 |
|
|
85
|
+
|
|
86
|
+
模型回退不携带工具,严格只接受完整 JSON `{ "action": "allow" }` 或 `{ "action": "deny" }`;缺路由、超时、错误、截断、非法 JSON 都失败关闭,不继续嵌套为 allow。支持用户手填模型 id,但 provider 必须已在宿主注册;这不是一个自动保存任意第三方 API Key 的新通道。
|
|
87
|
+
|
|
88
|
+
模型不能替真人批准 mandatory 操作。手动模式保留真人 ask。高任务相关性不是注入豁免,普通 captain→reviewer 祈使句也不再自动等同注入。注入分数是模型旁证而非确定性证据:自治软审查仍会交回退模型二审;不能据模拟模型结果宣布注入已彻底解决。
|
|
89
|
+
|
|
90
|
+
### 概率、缓存与审计
|
|
91
|
+
|
|
92
|
+
- `confidenceKind` 显式区分 normalized / provider_raw / unavailable。未声明尺度不参与风险阈值,不与 `P(choice)` 互换。
|
|
93
|
+
- 分类概率若出现,必须完整包含 low / medium / high,均在 [0,1] 且和为 1;畸形或缺失必要 noul 字段使建议 unknown。
|
|
94
|
+
- `risk_p` 仅保留 deprecated 原始 confidence 别名;新增 raw_decision_confidence、p_high、p_non_low、risk_severity。
|
|
95
|
+
- severity 是排序分数,分位数 ready 不是危险概率校准。`calibrated_p_unsafe` 保持 unknown/null。
|
|
96
|
+
- mandatory 每次重新检查,先于模型缓存;上下文版本进入精确请求键,授权不走软缓存。
|
|
97
|
+
- 授权的持久明细仅存摘要 hash 和结构化数值,不存原始目标、recent、工具参数或完整模型正文。发送到已配置模型的受限证据对象与持久对象分离,常见凭据字段先遮盖。
|
|
98
|
+
- 记录 mandatory / baseAction / tolerance / pluginReturnedAction / hostApprovalStatus / hostFinalAction / effectOutcome / degradedReason / cacheStatus;不存在的宿主证据写 unknown。
|
|
99
|
+
- 遥测追加失败会重试一次,再记录失败计数和日志警告;不把日志失败变成许可。
|
|
100
|
+
|
|
101
|
+
## 验证与证据
|
|
102
|
+
|
|
103
|
+
命令:
|
|
104
|
+
|
|
105
|
+
```powershell
|
|
106
|
+
vp install
|
|
107
|
+
vp run test
|
|
108
|
+
vp run verify:wiring
|
|
109
|
+
vp node --test test/guard-*.test.js test/scope*.test.js test/tolerance*.test.js
|
|
110
|
+
vp node scripts/verify-guard-scenarios.mjs bench/guard-revision
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- 最终安全专测 139 / 139 通过,合并 UI 专测 186 / 186 通过。全量 802 项中 797 通过、5 项失败;失败来自基线已有 Windows POSIX 权限断言、失败路径假设与临时目录清理竞态,未伪报全绿。逐项结果及原始日志见 `bench/guard-revision/validation.md`。
|
|
114
|
+
- 24 个独立合成情景按 8 良性 / 8 隔离高影响 / 8 来源权限对照冻结为 gold,四档共 96 次评估;输出 `gold.json`、`old-new.jsonl`、`report.json`。
|
|
115
|
+
- FULL 是合成 fixture 中可见的输入与管线事实,不是生产调用的充分观测;CD / NR 数量均为 0,不能据此比较历史 shadow 的分层指标。
|
|
116
|
+
- 合成良性 FPR 与高影响放行率见机器报告。高危使用真实工具名进入审核管线,但没有执行生产工具实现,真实危险 FNR 仍为 N/A,危险概率不可校准。不能将这些数字宣传成真实危险零漏杀。
|
|
117
|
+
- 旧结果列是 `decideGuardLegacy` + 容忍度纯函数反事实,不是旧 Host 最终动作;原 240 条 shadow 和 12 条现场录制未随此 checkout 提供,`_bench/gatebench-out` 不存在,无法重跑,未伪造数据。
|
|
118
|
+
- 浏览器访问给定现有 GUI 返回 authentication required;未搜索 token、未启动替代服务器、未生成 mock 页面。真实明暗主题、字体可用性与安装后的视觉验收仍未完成。
|
|
119
|
+
- 变更依赖的宿主 API 已 Inspect;repo wiring verifier 使用本地 peer 依赖与隔离 DSH_HOME,不等同运行安装的 E2E 验收。
|
|
120
|
+
|
|
121
|
+
## 分阶段发布、兼容与回滚
|
|
122
|
+
|
|
123
|
+
本次按可审查提交分别交付工具链、Fallback、安全核心、设置页与验证文档。为避免提交不可运行的临时授权链,PR-1~PR-5 的纯模块与接线在安全核心检查点合并验证,不声称已做五次生产 canary 发布。
|
|
124
|
+
|
|
125
|
+
1. 保持现有生产 profile 未安装、未修改;先在独立测试 profile 中观察记录。
|
|
126
|
+
2. 上线前补齐旧 shadow 原始资料,冻结业务允许的误报预算,验证宿主真实审批与实际工具结果,再做小范围 enforce。
|
|
127
|
+
3. `guardPolicyMode=observe` 是明确不实施 mandatory 的诊断降级,不能称作安全回滚;工具授权 observe/off 同样不提供保护。
|
|
128
|
+
4. 回滚源码用 `git revert <相应提交>`,保留历史与用户配置;不要恢复旧的 fail-open 旁路后仍标为安全版本。
|
|
129
|
+
5. 配置键保留兼容读取,但旧 read-only/exclude/outside 白名单不再越过 mandatory。旧统计记录不重写为新概率口径。
|
|
130
|
+
6. 不改本机全局权限、审批模式或默认模型;不自动安装或重启 DSH。
|
package/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 145 145" width="145" height="145">
|
|
3
|
+
<path d="M72 5.25C71.67 5.55 71.33 5.86 71 6.17C72.91 6.73 80.37 10.15 81 10.17C80.48 9.66 79.97 9.15 79.45 8.64C89.09 13.83 98.72 19.02 108.36 24.21C108.1 24.53 107.85 24.85 107.59 25.17C107.96 25.72 108.33 26.28 108.7 26.83C105.95 26.82 108.18 30.38 108.5 31.33C107.72 31.09 106.94 30.84 106.17 30.59C104.39 29.93 102.61 29.26 100.83 28.6C101.96 29.52 103.1 30.43 104.23 31.35C93.65 25.72 83.08 20.09 72.5 14.46C67.49 15.92 62.96 19.46 58.38 21.85C50.3 26.06 42.33 30.51 34.29 34.81C31.2 36.47 22.09 40.17 20.33 42.81C19.12 44.62 19.62 47.45 19.62 49.5C19.62 54.24 18.33 64.46 20.03 68.5C31.26 68.5 42.49 68.5 53.72 68.5C52.92 65.99 43.32 57.9 50.5 57.04C54.21 58.8 63.84 68.83 65.71 72.5C63.11 76.97 58.17 80.71 54.38 84.22C53.04 85.46 51.64 87.66 49.52 86.99C45.02 85.59 52.31 78.26 53.75 76.5C51.78 75.2 47.84 75.94 45.5 75.94C39.5 75.94 33.5 75.95 27.5 75.96C25.26 75.96 21.8 75.31 19.83 76.37C18.48 81.94 19.59 89.08 19.61 94.83C19.62 96.94 19.06 100.05 20.24 101.93C21.49 103.92 25.11 105.05 27.09 106.1C32.67 109.06 38.03 112.41 43.67 115.21C42.62 114.92 41.56 114.63 40.5 114.33C41.13 115.39 41.76 116.44 42.39 117.5C41.65 118.59 40.91 119.68 40.17 120.77C39.5 120.69 38.83 120.61 38.17 120.53C38.5 121.04 38.84 121.54 39.17 122.05C35.41 121.2 21.24 112.78 17.13 110.35C14.84 108.99 12.1 108.15 11.17 105.33C10.03 101.89 10.96 96.78 10.96 93.17C10.96 84.06 10.99 74.94 10.96 65.83C10.95 59.39 10.97 52.94 10.96 46.5C10.96 43.6 10.37 40.12 12.13 37.61C14.39 34.39 23.88 30.68 27.58 28.77C38.13 23.31 48.49 17.46 58.9 11.73C62.5 9.75 67.92 5.55 72 5.25ZM96.71 68.33C96.33 68.17 95.94 68 95.55 67.83C95.79 70.13 96.65 71.6 96.44 74.17C94.85 76.61 93.26 79.06 91.67 81.5C89.67 82.17 87.67 82.83 85.67 83.5C86.57 83.6 87.47 83.7 88.37 83.8C78.32 86.37 70.37 75.12 75.15 66.28C80.35 56.67 92.86 59.34 96.71 68.33ZM83.92 65.8C76.02 67.35 78.7 80.48 86.46 78.89C95.04 77.13 92.71 64.08 83.92 65.8Z" fill="#066066" fill-rule="evenodd" stroke="#066066" stroke-width="0.25" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M125.35 33.19C127.86 34.32 131.46 35.9 132.8 38.5C134.19 41.2 133.24 49.59 133.23 52.83C133.22 64.61 133.17 76.39 133.15 88.17C133.14 92.16 134.35 102.96 132.69 106.15C131 109.38 126.76 110.57 123.8 112.3C116.2 116.75 108.4 121.11 100.46 125.04C93 128.73 86.01 133.43 78.46 137.04C74.82 138.78 71.56 141.3 67.94 137.73C66.35 136.38 64.76 135.02 63.17 133.67C62.28 133.37 61.39 133.07 60.5 132.77C61.11 132.12 61.72 131.48 62.33 130.83C62.28 129.22 62.22 127.61 62.17 126C63.02 126.56 67.74 129.35 69.34 129.2C70.34 129.63 71.33 130.07 72.33 130.5C85.1 124.21 97.5 117.03 110.03 110.18C113.61 108.23 122.92 104.59 124.64 101.14C125.75 98.89 124.86 94.03 124.86 91.5C124.84 83.38 124.67 75.29 124.66 67.17C124.65 60.49 126.4 46.94 122.81 41.73C122.65 41.21 122.49 40.69 122.33 40.17C123.22 38.94 124.11 37.72 125 36.5C124.39 35.33 123.78 34.17 123.17 33C123.9 33.06 124.63 33.13 125.35 33.19ZM83.92 65.8C92.71 64.08 95.04 77.13 86.46 78.89C78.7 80.48 76.02 67.35 83.92 65.8Z" fill="#3aab8c" fill-rule="evenodd" stroke="#3aab8c" stroke-width="0.25" stroke-linejoin="round"/>
|
|
5
|
+
<path d="M79.45 8.64C79.97 9.15 80.48 9.66 81 10.17C80.37 10.15 72.91 6.73 71 6.17C71.33 5.86 71.67 5.55 72 5.25C73.79 4.92 78.32 7.28 79.45 8.64ZM108.36 24.21C114.02 27.2 119.69 30.2 125.35 33.19C124.63 33.13 123.9 33.06 123.17 33C123.78 34.17 124.39 35.33 125 36.5C124.11 37.72 123.22 38.94 122.33 40.17C122.49 40.69 122.65 41.21 122.81 41.73C116.62 38.27 110.42 34.81 104.23 31.35C103.1 30.43 101.96 29.52 100.83 28.6C102.61 29.26 104.39 29.93 106.17 30.59C106.94 30.84 107.72 31.09 108.5 31.33C108.18 30.38 105.95 26.82 108.7 26.83C108.33 26.28 107.96 25.72 107.59 25.17C107.85 24.85 108.1 24.53 108.36 24.21ZM96.71 68.33C99.82 74.08 94.39 82.79 88.37 83.8C87.47 83.7 86.57 83.6 85.67 83.5C87.67 82.83 89.67 82.17 91.67 81.5C93.26 79.06 94.85 76.61 96.44 74.17C96.65 71.6 95.79 70.13 95.55 67.83C95.94 68 96.33 68.17 96.71 68.33ZM43.67 115.21C52.23 119.87 60.78 124.53 69.34 129.2C67.74 129.35 63.02 126.56 62.17 126C62.22 127.61 62.28 129.22 62.33 130.83C61.72 131.48 61.11 132.12 60.5 132.77C61.39 133.07 62.28 133.37 63.17 133.67C64.76 135.02 66.35 136.38 67.94 137.73C58.35 132.51 48.76 127.28 39.17 122.05C38.84 121.54 38.5 121.04 38.17 120.53C38.83 120.61 39.5 120.69 40.17 120.77C40.91 119.68 41.65 118.59 42.39 117.5C41.76 116.44 41.13 115.39 40.5 114.33C41.56 114.63 42.62 114.92 43.67 115.21Z" fill="#1c7773" fill-rule="evenodd" stroke="#1c7773" stroke-width="0.25" stroke-linejoin="round"/>
|
|
6
|
+
</svg>
|