dsh-plugin-xujie-memory 0.5.0 → 0.7.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/ACCEPTANCE.md +37 -0
- package/BUILD-PROVENANCE.json +53 -0
- package/CHANGELOG.md +40 -1
- package/LICENSE +21 -21
- package/README.md +67 -17
- package/README_EN.md +54 -8
- package/cordis.patch.yml +49 -49
- package/lib/client.js +17219 -0
- package/lib/controller.js +307 -0
- package/lib/index.js +200 -14
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +15104 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.d.ts.map +1 -0
- package/lib/typert.remote-client.js +15093 -0
- package/lib/types.d.ts +13 -0
- package/lib/types.js +1 -0
- package/package.json +49 -5
- package/python/specs/characters/aloof.json +14 -14
- package/python/specs/characters/gentle.json +14 -14
- package/python/specs/characters/tsundere.json +14 -14
- package/python/specs/debt.v1.json +31 -31
- package/python/specs/nickname.v1.json +16 -16
- package/python/specs/preference.v1.json +16 -16
- package/python/xujie_memory/__init__.py +123 -123
- package/python/xujie_memory/admission.py +211 -211
- package/python/xujie_memory/audit.py +377 -377
- package/python/xujie_memory/benchmark.py +281 -281
- package/python/xujie_memory/character.py +199 -199
- package/python/xujie_memory/console.py +41 -41
- package/python/xujie_memory/consolidate.py +356 -356
- package/python/xujie_memory/due.py +121 -121
- package/python/xujie_memory/emotion.py +573 -573
- package/python/xujie_memory/engine.py +1333 -1312
- package/python/xujie_memory/errors.py +98 -98
- package/python/xujie_memory/importance.py +104 -104
- package/python/xujie_memory/judge.py +350 -350
- package/python/xujie_memory/management.py +203 -0
- package/python/xujie_memory/memory.py +71 -10
- package/python/xujie_memory/orchestrator.py +690 -666
- package/python/xujie_memory/phrasing.py +292 -292
- package/python/xujie_memory/privacy.py +105 -0
- package/python/xujie_memory/schema.sql +492 -480
- package/python/xujie_memory/server.py +76 -0
- package/python/xujie_memory/specs.py +170 -170
- package/python/xujie_memory/summary.py +238 -238
- package/scripts/build.mjs +19 -0
- package/scripts/prepack.mjs +9 -0
- package/scripts/test-all.mjs +9 -0
- package/scripts/test-node.mjs +10 -0
- package/src/client/MemoryPanel.tsx +536 -0
- package/src/client/NeuralField.tsx +88 -0
- package/src/client/css.d.ts +4 -0
- package/src/client/icons.tsx +28 -0
- package/src/client/index.tsx +59 -0
- package/src/client/memory-adapter.ts +67 -0
- package/src/client/model.ts +79 -0
- package/src/client/neural/engine.ts +999 -0
- package/src/client/neural/morphology.ts +219 -0
- package/src/client/panel.css +337 -0
- package/src/client/types.ts +6 -0
- package/src/controller.ts +88 -0
- package/src/generated/typert.host.js +449 -0
- package/src/generated/typert.remote-client.js +438 -0
- package/src/index.ts +2 -0
- package/src/types.ts +13 -0
- package/test/cordis-fixture.mjs +5 -0
- package/test/management_regression.py +187 -0
- package/test/node-regression.entry.mjs +52 -0
- package/vendor/DSH-LICENSE.txt +21 -0
- package/vendor/THIRD-PARTY.md +6 -0
- package/vendor/dsh-protocol/index.ts +484 -0
- package/vendor/dsh-protocol/json-value.ts +53 -0
- package/vendor/dsh-protocol/owned-value.ts +39 -0
- package/vendor/dsh-protocol/principal.ts +17 -0
- package/vendor/dsh-protocol/remote-access.ts +38 -0
- package/vendor/dsh-protocol/remote-error.ts +49 -0
- package/vendor/dsh-protocol/remote-info.ts +161 -0
- package/vendor/dsh-protocol/types.ts +939 -0
- package/vendor/licenses/jridgewell-gen-mapping-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-resolve-uri-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-sourcemap-codec-MIT.txt +19 -0
- package/vendor/licenses/jridgewell-trace-mapping-MIT.txt +19 -0
- package/vendor/licenses/zod-MIT.txt +21 -0
|
@@ -1,211 +1,211 @@
|
|
|
1
|
-
"""准入等级与熔断。
|
|
2
|
-
|
|
3
|
-
两件事,一个文件:
|
|
4
|
-
|
|
5
|
-
1. **晋级**:哪些句式可以不经用户点头就落库。
|
|
6
|
-
默认只放开三类"记账三原语"——有数量、无第三方、无歧义的
|
|
7
|
-
`incur / repay / waive`。其余全部 assisted。
|
|
8
|
-
依据是 G0 矩阵:`数量×口语` 100%、`数量×省略` 92%、`数量×指代` 91%,
|
|
9
|
-
而 `归属` 只有 60%——所以"带第三方"的一律不放开。
|
|
10
|
-
|
|
11
|
-
2. **熔断**:放开的格子一旦连续被纠正,自动收回。
|
|
12
|
-
这是"敢开"的前提——没有熔断的 auto 是赌博。
|
|
13
|
-
|
|
14
|
-
closed ──失败×3──► open ──确认×5──► closed
|
|
15
|
-
▲ │
|
|
16
|
-
└──────────────────┘
|
|
17
|
-
|
|
18
|
-
**为什么不用"连续正确 N 次就放开"**:没有线上抽检就攒不出"正确"的证据,
|
|
19
|
-
而"正确"是可以自证的(提交成功≠对)。但**"被纠正"是不可辩驳的证据**——
|
|
20
|
-
用户专门回来说"不是这个",那是系统错了。所以晋级靠人工判断(白名单),
|
|
21
|
-
熔断靠用户行为(自动)。
|
|
22
|
-
|
|
23
|
-
这条取舍写在这里,是因为它决定了:**系统会自己变紧,不会自己变松。**
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
import sqlite3
|
|
29
|
-
from typing import Any
|
|
30
|
-
|
|
31
|
-
from .engine import Engine, canonical_json
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# 默认放开的句式。加一个进来要能回答两个问题:
|
|
35
|
-
# 1. 这句话的语义有几种可能?(只有一种才能放开)
|
|
36
|
-
# 2. 错了会怎样?(会改变余额,但改错能立刻看出来)
|
|
37
|
-
DEFAULT_AUTO = frozenset({
|
|
38
|
-
"incur:plain", # 我欠你三袋
|
|
39
|
-
"repay:plain", # 还你两袋
|
|
40
|
-
"waive:plain", # 免一袋
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
# 永远不放开的。即使有人往 DEFAULT_AUTO 里加,这几个也挡在前面。
|
|
44
|
-
NEVER_AUTO = frozenset({
|
|
45
|
-
"correct:revision", # 改历史
|
|
46
|
-
"retract:revision", # 撤历史
|
|
47
|
-
"set_total:revision", # 改写总额
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class AdmissionController:
|
|
52
|
-
"""按 (关系, 句式) 决定准入等级,并维护熔断状态。"""
|
|
53
|
-
|
|
54
|
-
FAIL_THRESHOLD = 3 # 连续被纠正几次 → 熔断
|
|
55
|
-
RELEASE_AFTER = 5 # 熔断后连续确认几次 → 恢复
|
|
56
|
-
|
|
57
|
-
def __init__(self, engine: Engine):
|
|
58
|
-
self.engine = engine
|
|
59
|
-
|
|
60
|
-
# ------------------------------------------------------------ 分类
|
|
61
|
-
@staticmethod
|
|
62
|
-
def pattern_of(op: str, has_qty: bool, signals: dict[str, Any],
|
|
63
|
-
modality: str = "asserted") -> str:
|
|
64
|
-
"""这句话属于哪一类风险。**顺序就是优先级。**
|
|
65
|
-
|
|
66
|
-
先看"会不会改历史"(最危险),再看"施事/语气/性质",
|
|
67
|
-
最后才看"有没有数量"。反过来的话,「替老张赊了三袋」
|
|
68
|
-
会因为"有数量"被归到 plain 里——而它恰恰是最容易记错人的那一类。
|
|
69
|
-
"""
|
|
70
|
-
if op in ("correct", "retract", "set_total"):
|
|
71
|
-
return f"{op}:revision"
|
|
72
|
-
# 请求不是事实。「给我免三碗」是"我想要",不是"已经免了"——
|
|
73
|
-
# 放它自动提交,等于替对方答应了一件事。
|
|
74
|
-
if modality in ("request", "planned"):
|
|
75
|
-
return f"{op}:request"
|
|
76
|
-
if signals.get("third_party"):
|
|
77
|
-
return f"{op}:third_party"
|
|
78
|
-
if signals.get("settled_report") or signals.get("balance_check"):
|
|
79
|
-
return f"{op}:check"
|
|
80
|
-
if signals.get("seeking_agreement"):
|
|
81
|
-
return f"{op}:check"
|
|
82
|
-
if signals.get("describe_only"):
|
|
83
|
-
return f"{op}:describe"
|
|
84
|
-
if op == "promise":
|
|
85
|
-
return "promise:plain" # 不改余额,本来就 record_only
|
|
86
|
-
if not has_qty:
|
|
87
|
-
return f"{op}:noqty" # 没数量,多半要问
|
|
88
|
-
return f"{op}:plain"
|
|
89
|
-
|
|
90
|
-
def _default_level(self, pattern: str) -> str:
|
|
91
|
-
if pattern in NEVER_AUTO:
|
|
92
|
-
return "assisted"
|
|
93
|
-
return "auto" if pattern in DEFAULT_AUTO else "assisted"
|
|
94
|
-
|
|
95
|
-
# ------------------------------------------------------------ 查询
|
|
96
|
-
def state_of(self, relation: str, pattern: str) -> dict[str, Any]:
|
|
97
|
-
row = self.engine._conn().execute(
|
|
98
|
-
"SELECT * FROM admission_state WHERE relation=? AND pattern=?",
|
|
99
|
-
(relation, pattern),
|
|
100
|
-
).fetchone()
|
|
101
|
-
if row:
|
|
102
|
-
return dict(row)
|
|
103
|
-
return {
|
|
104
|
-
"relation": relation, "pattern": pattern,
|
|
105
|
-
"level": self._default_level(pattern),
|
|
106
|
-
"failures": 0, "successes": 0, "opened_at": None,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
def level(self, relation: str, pattern: str, spec_mode: str,
|
|
110
|
-
escalate: bool = True) -> str:
|
|
111
|
-
"""最终等级。**规格是上限,不是下限。**
|
|
112
|
-
|
|
113
|
-
spec 说 shadow,就永远 shadow——句式再稳也不能越过关系的授权级别。
|
|
114
|
-
spec 说 assisted,白名单里的句式可以升到 auto;
|
|
115
|
-
白名单外的停留在 assisted。
|
|
116
|
-
"""
|
|
117
|
-
if spec_mode == "shadow":
|
|
118
|
-
return "shadow"
|
|
119
|
-
|
|
120
|
-
# 熔断优先级最高:格子被关掉就是关掉,规格说 auto 也不给开。
|
|
121
|
-
# 这是"熔断"这个词的全部意义——**它必须能压过配置**,
|
|
122
|
-
# 否则一次配置错误就再也收不回来。
|
|
123
|
-
if self.state_of(relation, pattern).get("opened_at"):
|
|
124
|
-
return "assisted"
|
|
125
|
-
|
|
126
|
-
if spec_mode == "auto":
|
|
127
|
-
return "auto"
|
|
128
|
-
|
|
129
|
-
# 规格是 assisted:只有白名单里的句式能升。
|
|
130
|
-
# escalate=False 用于回放/测试——那两种场景要的是"规则原样",
|
|
131
|
-
# 不是"当前部署的准入配置"。
|
|
132
|
-
if not escalate:
|
|
133
|
-
return "assisted"
|
|
134
|
-
return "auto" if pattern in DEFAULT_AUTO else "assisted"
|
|
135
|
-
|
|
136
|
-
# ------------------------------------------------------------ 熔断
|
|
137
|
-
def record_failure(
|
|
138
|
-
self, scope_id: str, relation: str, pattern: str, reason: str
|
|
139
|
-
) -> bool:
|
|
140
|
-
"""记一次"被纠正"。返回 True 表示**这一次把格子关掉了**。"""
|
|
141
|
-
st = self.state_of(relation, pattern)
|
|
142
|
-
failures = int(st.get("failures") or 0) + 1
|
|
143
|
-
now = self.engine.now()
|
|
144
|
-
opened = st.get("opened_at")
|
|
145
|
-
|
|
146
|
-
if failures >= self.FAIL_THRESHOLD and not opened:
|
|
147
|
-
opened = now
|
|
148
|
-
self._upsert(relation, pattern, "assisted", failures, 0, opened)
|
|
149
|
-
self._log(scope_id, relation, pattern, "open",
|
|
150
|
-
f"连续 {failures} 次被纠正:{reason}")
|
|
151
|
-
return True
|
|
152
|
-
|
|
153
|
-
self._upsert(relation, pattern, st.get("level") or "assisted",
|
|
154
|
-
failures, int(st.get("successes") or 0), opened)
|
|
155
|
-
self._log(scope_id, relation, pattern, "failure", reason)
|
|
156
|
-
return False
|
|
157
|
-
|
|
158
|
-
def record_success(self, relation: str, pattern: str) -> bool:
|
|
159
|
-
"""记一次"用户确认了"。只在熔断后计数,用于恢复。返回 True 表示恢复了。"""
|
|
160
|
-
st = self.state_of(relation, pattern)
|
|
161
|
-
if not st.get("opened_at"):
|
|
162
|
-
return False # 没熔断就不用记
|
|
163
|
-
successes = int(st.get("successes") or 0) + 1
|
|
164
|
-
if successes >= self.RELEASE_AFTER:
|
|
165
|
-
self._upsert(relation, pattern, self._default_level(pattern), 0, 0, None)
|
|
166
|
-
self._log("", relation, pattern, "close",
|
|
167
|
-
f"连续 {successes} 次确认,恢复")
|
|
168
|
-
return True
|
|
169
|
-
self._upsert(relation, pattern, "assisted", int(st.get("failures") or 0),
|
|
170
|
-
successes, st["opened_at"])
|
|
171
|
-
return False
|
|
172
|
-
|
|
173
|
-
# ------------------------------------------------------------ 存储
|
|
174
|
-
def _upsert(self, relation: str, pattern: str, level: str,
|
|
175
|
-
failures: int, successes: int, opened_at: str | None) -> None:
|
|
176
|
-
# 走 engine._tx():它可重入(SAVEPOINT),所以在 _apply_revision
|
|
177
|
-
# 的事务里调用也不会炸。**不能自己 commit**——那会把外层事务提前提交。
|
|
178
|
-
with self.engine._tx() as conn:
|
|
179
|
-
conn.execute(
|
|
180
|
-
"INSERT INTO admission_state(relation, pattern, level, failures,"
|
|
181
|
-
" successes, opened_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
|
182
|
-
" ON CONFLICT(relation, pattern) DO UPDATE SET"
|
|
183
|
-
" level=excluded.level, failures=excluded.failures,"
|
|
184
|
-
" successes=excluded.successes, opened_at=excluded.opened_at,"
|
|
185
|
-
" updated_at=excluded.updated_at",
|
|
186
|
-
(relation, pattern, level, failures, successes, opened_at,
|
|
187
|
-
self.engine.now()),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
def _log(self, scope_id: str, relation: str, pattern: str,
|
|
191
|
-
action: str, reason: str) -> None:
|
|
192
|
-
with self.engine._tx() as conn:
|
|
193
|
-
conn.execute(
|
|
194
|
-
"INSERT INTO breaker_log(log_id, scope_id, relation, pattern,"
|
|
195
|
-
" action, reason, at) VALUES (?,?,?,?,?,?,?)",
|
|
196
|
-
(f"bl_{abs(hash((relation, pattern, action, self.engine.now()))) % 10**12}",
|
|
197
|
-
scope_id or "-", relation, pattern, action, reason, self.engine.now()),
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
# ------------------------------------------------------------ 读
|
|
201
|
-
def snapshot(self) -> list[dict[str, Any]]:
|
|
202
|
-
rows = self.engine._conn().execute(
|
|
203
|
-
"SELECT * FROM admission_state ORDER BY relation, pattern"
|
|
204
|
-
).fetchall()
|
|
205
|
-
return [dict(r) for r in rows]
|
|
206
|
-
|
|
207
|
-
def log(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
208
|
-
rows = self.engine._conn().execute(
|
|
209
|
-
"SELECT * FROM breaker_log ORDER BY at DESC LIMIT ?", (limit,)
|
|
210
|
-
).fetchall()
|
|
211
|
-
return [dict(r) for r in rows]
|
|
1
|
+
"""准入等级与熔断。
|
|
2
|
+
|
|
3
|
+
两件事,一个文件:
|
|
4
|
+
|
|
5
|
+
1. **晋级**:哪些句式可以不经用户点头就落库。
|
|
6
|
+
默认只放开三类"记账三原语"——有数量、无第三方、无歧义的
|
|
7
|
+
`incur / repay / waive`。其余全部 assisted。
|
|
8
|
+
依据是 G0 矩阵:`数量×口语` 100%、`数量×省略` 92%、`数量×指代` 91%,
|
|
9
|
+
而 `归属` 只有 60%——所以"带第三方"的一律不放开。
|
|
10
|
+
|
|
11
|
+
2. **熔断**:放开的格子一旦连续被纠正,自动收回。
|
|
12
|
+
这是"敢开"的前提——没有熔断的 auto 是赌博。
|
|
13
|
+
|
|
14
|
+
closed ──失败×3──► open ──确认×5──► closed
|
|
15
|
+
▲ │
|
|
16
|
+
└──────────────────┘
|
|
17
|
+
|
|
18
|
+
**为什么不用"连续正确 N 次就放开"**:没有线上抽检就攒不出"正确"的证据,
|
|
19
|
+
而"正确"是可以自证的(提交成功≠对)。但**"被纠正"是不可辩驳的证据**——
|
|
20
|
+
用户专门回来说"不是这个",那是系统错了。所以晋级靠人工判断(白名单),
|
|
21
|
+
熔断靠用户行为(自动)。
|
|
22
|
+
|
|
23
|
+
这条取舍写在这里,是因为它决定了:**系统会自己变紧,不会自己变松。**
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sqlite3
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from .engine import Engine, canonical_json
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# 默认放开的句式。加一个进来要能回答两个问题:
|
|
35
|
+
# 1. 这句话的语义有几种可能?(只有一种才能放开)
|
|
36
|
+
# 2. 错了会怎样?(会改变余额,但改错能立刻看出来)
|
|
37
|
+
DEFAULT_AUTO = frozenset({
|
|
38
|
+
"incur:plain", # 我欠你三袋
|
|
39
|
+
"repay:plain", # 还你两袋
|
|
40
|
+
"waive:plain", # 免一袋
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
# 永远不放开的。即使有人往 DEFAULT_AUTO 里加,这几个也挡在前面。
|
|
44
|
+
NEVER_AUTO = frozenset({
|
|
45
|
+
"correct:revision", # 改历史
|
|
46
|
+
"retract:revision", # 撤历史
|
|
47
|
+
"set_total:revision", # 改写总额
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AdmissionController:
|
|
52
|
+
"""按 (关系, 句式) 决定准入等级,并维护熔断状态。"""
|
|
53
|
+
|
|
54
|
+
FAIL_THRESHOLD = 3 # 连续被纠正几次 → 熔断
|
|
55
|
+
RELEASE_AFTER = 5 # 熔断后连续确认几次 → 恢复
|
|
56
|
+
|
|
57
|
+
def __init__(self, engine: Engine):
|
|
58
|
+
self.engine = engine
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------ 分类
|
|
61
|
+
@staticmethod
|
|
62
|
+
def pattern_of(op: str, has_qty: bool, signals: dict[str, Any],
|
|
63
|
+
modality: str = "asserted") -> str:
|
|
64
|
+
"""这句话属于哪一类风险。**顺序就是优先级。**
|
|
65
|
+
|
|
66
|
+
先看"会不会改历史"(最危险),再看"施事/语气/性质",
|
|
67
|
+
最后才看"有没有数量"。反过来的话,「替老张赊了三袋」
|
|
68
|
+
会因为"有数量"被归到 plain 里——而它恰恰是最容易记错人的那一类。
|
|
69
|
+
"""
|
|
70
|
+
if op in ("correct", "retract", "set_total"):
|
|
71
|
+
return f"{op}:revision"
|
|
72
|
+
# 请求不是事实。「给我免三碗」是"我想要",不是"已经免了"——
|
|
73
|
+
# 放它自动提交,等于替对方答应了一件事。
|
|
74
|
+
if modality in ("request", "planned"):
|
|
75
|
+
return f"{op}:request"
|
|
76
|
+
if signals.get("third_party"):
|
|
77
|
+
return f"{op}:third_party"
|
|
78
|
+
if signals.get("settled_report") or signals.get("balance_check"):
|
|
79
|
+
return f"{op}:check"
|
|
80
|
+
if signals.get("seeking_agreement"):
|
|
81
|
+
return f"{op}:check"
|
|
82
|
+
if signals.get("describe_only"):
|
|
83
|
+
return f"{op}:describe"
|
|
84
|
+
if op == "promise":
|
|
85
|
+
return "promise:plain" # 不改余额,本来就 record_only
|
|
86
|
+
if not has_qty:
|
|
87
|
+
return f"{op}:noqty" # 没数量,多半要问
|
|
88
|
+
return f"{op}:plain"
|
|
89
|
+
|
|
90
|
+
def _default_level(self, pattern: str) -> str:
|
|
91
|
+
if pattern in NEVER_AUTO:
|
|
92
|
+
return "assisted"
|
|
93
|
+
return "auto" if pattern in DEFAULT_AUTO else "assisted"
|
|
94
|
+
|
|
95
|
+
# ------------------------------------------------------------ 查询
|
|
96
|
+
def state_of(self, relation: str, pattern: str) -> dict[str, Any]:
|
|
97
|
+
row = self.engine._conn().execute(
|
|
98
|
+
"SELECT * FROM admission_state WHERE relation=? AND pattern=?",
|
|
99
|
+
(relation, pattern),
|
|
100
|
+
).fetchone()
|
|
101
|
+
if row:
|
|
102
|
+
return dict(row)
|
|
103
|
+
return {
|
|
104
|
+
"relation": relation, "pattern": pattern,
|
|
105
|
+
"level": self._default_level(pattern),
|
|
106
|
+
"failures": 0, "successes": 0, "opened_at": None,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def level(self, relation: str, pattern: str, spec_mode: str,
|
|
110
|
+
escalate: bool = True) -> str:
|
|
111
|
+
"""最终等级。**规格是上限,不是下限。**
|
|
112
|
+
|
|
113
|
+
spec 说 shadow,就永远 shadow——句式再稳也不能越过关系的授权级别。
|
|
114
|
+
spec 说 assisted,白名单里的句式可以升到 auto;
|
|
115
|
+
白名单外的停留在 assisted。
|
|
116
|
+
"""
|
|
117
|
+
if spec_mode == "shadow":
|
|
118
|
+
return "shadow"
|
|
119
|
+
|
|
120
|
+
# 熔断优先级最高:格子被关掉就是关掉,规格说 auto 也不给开。
|
|
121
|
+
# 这是"熔断"这个词的全部意义——**它必须能压过配置**,
|
|
122
|
+
# 否则一次配置错误就再也收不回来。
|
|
123
|
+
if self.state_of(relation, pattern).get("opened_at"):
|
|
124
|
+
return "assisted"
|
|
125
|
+
|
|
126
|
+
if spec_mode == "auto":
|
|
127
|
+
return "auto"
|
|
128
|
+
|
|
129
|
+
# 规格是 assisted:只有白名单里的句式能升。
|
|
130
|
+
# escalate=False 用于回放/测试——那两种场景要的是"规则原样",
|
|
131
|
+
# 不是"当前部署的准入配置"。
|
|
132
|
+
if not escalate:
|
|
133
|
+
return "assisted"
|
|
134
|
+
return "auto" if pattern in DEFAULT_AUTO else "assisted"
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------ 熔断
|
|
137
|
+
def record_failure(
|
|
138
|
+
self, scope_id: str, relation: str, pattern: str, reason: str
|
|
139
|
+
) -> bool:
|
|
140
|
+
"""记一次"被纠正"。返回 True 表示**这一次把格子关掉了**。"""
|
|
141
|
+
st = self.state_of(relation, pattern)
|
|
142
|
+
failures = int(st.get("failures") or 0) + 1
|
|
143
|
+
now = self.engine.now()
|
|
144
|
+
opened = st.get("opened_at")
|
|
145
|
+
|
|
146
|
+
if failures >= self.FAIL_THRESHOLD and not opened:
|
|
147
|
+
opened = now
|
|
148
|
+
self._upsert(relation, pattern, "assisted", failures, 0, opened)
|
|
149
|
+
self._log(scope_id, relation, pattern, "open",
|
|
150
|
+
f"连续 {failures} 次被纠正:{reason}")
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
self._upsert(relation, pattern, st.get("level") or "assisted",
|
|
154
|
+
failures, int(st.get("successes") or 0), opened)
|
|
155
|
+
self._log(scope_id, relation, pattern, "failure", reason)
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def record_success(self, relation: str, pattern: str) -> bool:
|
|
159
|
+
"""记一次"用户确认了"。只在熔断后计数,用于恢复。返回 True 表示恢复了。"""
|
|
160
|
+
st = self.state_of(relation, pattern)
|
|
161
|
+
if not st.get("opened_at"):
|
|
162
|
+
return False # 没熔断就不用记
|
|
163
|
+
successes = int(st.get("successes") or 0) + 1
|
|
164
|
+
if successes >= self.RELEASE_AFTER:
|
|
165
|
+
self._upsert(relation, pattern, self._default_level(pattern), 0, 0, None)
|
|
166
|
+
self._log("", relation, pattern, "close",
|
|
167
|
+
f"连续 {successes} 次确认,恢复")
|
|
168
|
+
return True
|
|
169
|
+
self._upsert(relation, pattern, "assisted", int(st.get("failures") or 0),
|
|
170
|
+
successes, st["opened_at"])
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------ 存储
|
|
174
|
+
def _upsert(self, relation: str, pattern: str, level: str,
|
|
175
|
+
failures: int, successes: int, opened_at: str | None) -> None:
|
|
176
|
+
# 走 engine._tx():它可重入(SAVEPOINT),所以在 _apply_revision
|
|
177
|
+
# 的事务里调用也不会炸。**不能自己 commit**——那会把外层事务提前提交。
|
|
178
|
+
with self.engine._tx() as conn:
|
|
179
|
+
conn.execute(
|
|
180
|
+
"INSERT INTO admission_state(relation, pattern, level, failures,"
|
|
181
|
+
" successes, opened_at, updated_at) VALUES (?,?,?,?,?,?,?)"
|
|
182
|
+
" ON CONFLICT(relation, pattern) DO UPDATE SET"
|
|
183
|
+
" level=excluded.level, failures=excluded.failures,"
|
|
184
|
+
" successes=excluded.successes, opened_at=excluded.opened_at,"
|
|
185
|
+
" updated_at=excluded.updated_at",
|
|
186
|
+
(relation, pattern, level, failures, successes, opened_at,
|
|
187
|
+
self.engine.now()),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def _log(self, scope_id: str, relation: str, pattern: str,
|
|
191
|
+
action: str, reason: str) -> None:
|
|
192
|
+
with self.engine._tx() as conn:
|
|
193
|
+
conn.execute(
|
|
194
|
+
"INSERT INTO breaker_log(log_id, scope_id, relation, pattern,"
|
|
195
|
+
" action, reason, at) VALUES (?,?,?,?,?,?,?)",
|
|
196
|
+
(f"bl_{abs(hash((relation, pattern, action, self.engine.now()))) % 10**12}",
|
|
197
|
+
scope_id or "-", relation, pattern, action, reason, self.engine.now()),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# ------------------------------------------------------------ 读
|
|
201
|
+
def snapshot(self) -> list[dict[str, Any]]:
|
|
202
|
+
rows = self.engine._conn().execute(
|
|
203
|
+
"SELECT * FROM admission_state ORDER BY relation, pattern"
|
|
204
|
+
).fetchall()
|
|
205
|
+
return [dict(r) for r in rows]
|
|
206
|
+
|
|
207
|
+
def log(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
208
|
+
rows = self.engine._conn().execute(
|
|
209
|
+
"SELECT * FROM breaker_log ORDER BY at DESC LIMIT ?", (limit,)
|
|
210
|
+
).fetchall()
|
|
211
|
+
return [dict(r) for r in rows]
|