dsh-deepmoyu 2.7.2 → 2.8.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/AGENTS.md +9 -0
- package/CHANGELOG.md +36 -0
- package/CONTRIBUTING.md +1 -0
- package/README.md +12 -3
- package/client.js +430 -293
- package/docs/ACTIVITY_TUNING.md +50 -0
- package/docs/CONTINUOUS_BROWSING.md +35 -0
- package/docs/DEFAULTS_2026_11_13.md +48 -0
- package/docs/FUN_FEATURE_REFERENCES.md +128 -0
- package/docs/LOCAL_COMPAT_AUDIT_2026-11-12.md +58 -0
- package/docs/NATURAL_AUTONOMY.md +91 -0
- package/docs/PRIVACY.md +7 -0
- package/docs/RELEASE_2_8_0.md +27 -0
- package/docs/RELEASE_2_8_1.md +29 -0
- package/docs/RELEASE_AUDIT.md +22 -0
- package/docs/TEST_MATRIX.md +9 -0
- package/docs/WORKFLOW_REFERENCES.md +57 -0
- package/docs/tool-evidence.json +6 -1
- package/index.js +693 -241
- package/lib/background-tools.mjs +1 -1
- package/lib/browse-preferences.mjs +154 -0
- package/lib/browse-progress.mjs +165 -0
- package/lib/comment.mjs +29 -46
- package/lib/config-policy.mjs +2 -1
- package/lib/curiosity.mjs +153 -0
- package/lib/learn-review.mjs +2 -2
- package/lib/learn-store.mjs +7 -0
- package/lib/learn.mjs +234 -69
- package/lib/mcp.mjs +5 -2
- package/lib/memory-center.mjs +21 -19
- package/lib/mode.mjs +23 -9
- package/lib/model-relay-adapter.mjs +1 -1
- package/lib/model-relay.mjs +15 -2
- package/lib/pace.mjs +15 -7
- package/lib/persona.mjs +44 -29
- package/lib/product-status.mjs +38 -2
- package/lib/prompts.mjs +54 -39
- package/lib/pv.py +26 -12
- package/lib/pv_editorial.py +5 -2
- package/lib/run-settings.mjs +5 -5
- package/lib/session-continuation.mjs +47 -0
- package/package.json +1 -1
- package/presets/deepmoyu.patch.yml +10 -11
- package/prompts/duty.md +35 -37
- package/vendor/bilibili-mcp/mcp_server.py +88 -15
- package/vendor/bilibili-mcp/runtime_guard.py +132 -25
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Durable, cross-process deduplication for both Bilibili MCP entry paths.
|
|
2
2
|
|
|
3
|
-
Only state.json and seen.md are read for migration.
|
|
4
|
-
|
|
3
|
+
Only state.json and seen.md are read for migration. Public subtitle snapshots are
|
|
4
|
+
cached separately from permanent history. No credentials or private messages are
|
|
5
|
+
read into this database. Send reservations never expire automatically: after a
|
|
5
6
|
crash or an ambiguous network outcome, retrying could publish a second message.
|
|
6
7
|
"""
|
|
7
8
|
from __future__ import annotations
|
|
@@ -22,14 +23,16 @@ from pathlib import Path
|
|
|
22
23
|
BV_RE = re.compile(r"^BV[0-9A-Za-z]{10}$")
|
|
23
24
|
READ_LEASE_SECONDS = 600
|
|
24
25
|
READ_TIMEOUT_SECONDS = 180
|
|
26
|
+
SUBTITLE_CACHE_MAX = 256
|
|
27
|
+
SUBTITLE_CACHE_BYTES = 2_000_000
|
|
25
28
|
|
|
26
29
|
# Same limits as lib/pace.mjs; SQLite is the common floor for GUI and native MCP.
|
|
27
30
|
PACE = {
|
|
28
|
-
"comment": {"gap": 120, "jitter": 60, "hour":
|
|
29
|
-
"reply": {"gap": 60, "jitter": 30, "hour":
|
|
30
|
-
"opusReply": {"gap": 20, "jitter": 10, "hour":
|
|
31
|
-
"dm": {"gap": 30, "jitter": 15, "hour":
|
|
32
|
-
"moment": {"gap": 600, "jitter": 300, "hour":
|
|
31
|
+
"comment": {"gap": 120, "jitter": 60, "hour": 16},
|
|
32
|
+
"reply": {"gap": 60, "jitter": 30, "hour": 30},
|
|
33
|
+
"opusReply": {"gap": 20, "jitter": 10, "hour": 40},
|
|
34
|
+
"dm": {"gap": 30, "jitter": 15, "hour": 30},
|
|
35
|
+
"moment": {"gap": 600, "jitter": 300, "hour": 2},
|
|
33
36
|
}
|
|
34
37
|
BURST_WINDOW, BURST_MAX, BURST_EXTRA = 600, 10, 180
|
|
35
38
|
PACE_KEEP = 7200
|
|
@@ -45,6 +48,10 @@ def _finite(value, default=0):
|
|
|
45
48
|
return float(default)
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
def _pace_token(value):
|
|
52
|
+
return value.lower() if isinstance(value, str) and re.fullmatch(r"[0-9a-fA-F]{32}", value) else ""
|
|
53
|
+
|
|
54
|
+
|
|
48
55
|
def pace_kind(key):
|
|
49
56
|
prefix = str(key).split(":", 1)[0]
|
|
50
57
|
return {"nu": "comment", "video": "reply", "opus": "opusReply",
|
|
@@ -238,6 +245,10 @@ class RuntimeGuard:
|
|
|
238
245
|
conn.execute("CREATE INDEX IF NOT EXISTS guard_pace_kind_at ON guard_pace_events(kind,at)")
|
|
239
246
|
conn.execute("CREATE TABLE IF NOT EXISTS guard_pace_state ("
|
|
240
247
|
"name TEXT PRIMARY KEY, value TEXT NOT NULL)")
|
|
248
|
+
conn.execute("CREATE TABLE IF NOT EXISTS subtitle_cache ("
|
|
249
|
+
"bvid TEXT PRIMARY KEY, fetched REAL NOT NULL, payload TEXT NOT NULL)")
|
|
250
|
+
conn.execute("CREATE TABLE IF NOT EXISTS subtitle_reads ("
|
|
251
|
+
"bvid TEXT PRIMARY KEY, token TEXT NOT NULL, expires REAL NOT NULL)")
|
|
241
252
|
finally:
|
|
242
253
|
conn.close()
|
|
243
254
|
|
|
@@ -305,17 +316,19 @@ class RuntimeGuard:
|
|
|
305
316
|
if isinstance(row, dict):
|
|
306
317
|
at = _finite(row.get("at"))
|
|
307
318
|
nxt = _finite(row.get("next"))
|
|
308
|
-
stored.append((kind, at, nxt if nxt > 0 else None))
|
|
319
|
+
stored.append((kind, at, nxt if nxt > 0 else None, row.get("token") if "token" in row else None))
|
|
309
320
|
else:
|
|
310
|
-
stored.append((kind, _finite(row), None))
|
|
321
|
+
stored.append((kind, _finite(row), None, None))
|
|
311
322
|
|
|
312
|
-
def recorded_next(kind, at):
|
|
313
|
-
for s_kind, s_at, s_next in stored:
|
|
314
|
-
|
|
323
|
+
def recorded_next(kind, at, token):
|
|
324
|
+
for s_kind, s_at, s_next, s_token in stored:
|
|
325
|
+
same_event = (_pace_token(token) == _pace_token(s_token) and bool(_pace_token(token))
|
|
326
|
+
if token is not None else s_token is None and abs(s_at - at) <= 2000)
|
|
327
|
+
if s_kind == kind and s_next and same_event:
|
|
315
328
|
return s_next
|
|
316
329
|
return None
|
|
317
330
|
|
|
318
|
-
def event(kind, stamp, next_stamp=None, pinned=False):
|
|
331
|
+
def event(kind, stamp, next_stamp=None, pinned=False, token=None):
|
|
319
332
|
at = _finite(stamp) / 1000
|
|
320
333
|
if kind not in PACE or at <= 0:
|
|
321
334
|
return
|
|
@@ -323,12 +336,24 @@ class RuntimeGuard:
|
|
|
323
336
|
free = max(at, free)
|
|
324
337
|
if at < now - PACE_KEEP and free <= now:
|
|
325
338
|
return
|
|
326
|
-
#
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
339
|
+
# The host may finish its state lock seconds after Python committed. A receipt
|
|
340
|
+
# token identifies that exact send without consuming another hourly/burst slot.
|
|
341
|
+
# Only old records with no token use the legacy two-second migration heuristic.
|
|
342
|
+
valid_token = _pace_token(token)
|
|
343
|
+
if valid_token:
|
|
344
|
+
native = conn.execute("SELECT event_key,next FROM guard_pace_events "
|
|
345
|
+
"WHERE kind=? AND status='sent' AND token=? LIMIT 1",
|
|
346
|
+
(kind, valid_token)).fetchone()
|
|
347
|
+
elif token is None:
|
|
348
|
+
native = conn.execute("SELECT event_key,next FROM guard_pace_events "
|
|
349
|
+
"WHERE kind=? AND status='sent' AND token<>'' AND ABS(at-?)<=2 "
|
|
350
|
+
"ORDER BY ABS(at-?) LIMIT 1", (kind, at, at)).fetchone()
|
|
351
|
+
else:
|
|
352
|
+
native = None
|
|
331
353
|
if native:
|
|
354
|
+
if valid_token:
|
|
355
|
+
conn.execute("DELETE FROM guard_pace_events WHERE kind=? AND status='seed' AND token=?",
|
|
356
|
+
(kind, valid_token))
|
|
332
357
|
# The host already pinned this interval (one random draw, shown in the GUI).
|
|
333
358
|
# Python's own independent draw must not silently lengthen it — otherwise the
|
|
334
359
|
# UI says "40s left" while the native guard still waits 90s.
|
|
@@ -339,19 +364,20 @@ class RuntimeGuard:
|
|
|
339
364
|
conn.execute("UPDATE guard_pace_events SET next=MAX(next,?) WHERE event_key=?",
|
|
340
365
|
(free, native["event_key"]))
|
|
341
366
|
return
|
|
342
|
-
key = f"seed:{kind}:{int(round(at * 1000))}"
|
|
367
|
+
key = f"seed:{kind}:token:{valid_token}" if valid_token else f"seed:{kind}:{int(round(at * 1000))}"
|
|
343
368
|
conn.execute("INSERT INTO guard_pace_events(event_key,kind,at,next,status,token) "
|
|
344
|
-
"VALUES (?,?,?,?,'seed'
|
|
345
|
-
(key, kind, at, free))
|
|
369
|
+
"VALUES (?,?,?,?,'seed',?) ON CONFLICT(event_key) DO UPDATE SET next=MAX(next,excluded.next)",
|
|
370
|
+
(key, kind, at, free, valid_token))
|
|
346
371
|
|
|
347
372
|
recent = raw.get("recent")
|
|
348
373
|
for row in (recent[-200:] if isinstance(recent, list) else []):
|
|
349
374
|
if isinstance(row, dict):
|
|
350
375
|
kind, at = str(row.get("k") or ""), _finite(row.get("t"))
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
376
|
+
token = row.get("token") if "token" in row else None
|
|
377
|
+
nxt = recorded_next(kind, at, token)
|
|
378
|
+
event(kind, at, nxt, pinned=nxt is not None, token=token)
|
|
379
|
+
for kind, at, nxt, token in stored:
|
|
380
|
+
event(kind, at, nxt, pinned=nxt is not None, token=token)
|
|
355
381
|
book = self._pace_state(conn)
|
|
356
382
|
cool = _finite(raw.get("cooldownUntil")) / 1000
|
|
357
383
|
if cool > book["cooldown_until"]:
|
|
@@ -594,6 +620,81 @@ class RuntimeGuard:
|
|
|
594
620
|
raise ValueError("invalid bvid")
|
|
595
621
|
return self._reserve("seen:" + bvid, "read")
|
|
596
622
|
|
|
623
|
+
def cached_subtitle(self, bvid):
|
|
624
|
+
"""Read a bounded public transcript; history alone is never transcript evidence."""
|
|
625
|
+
bvid = str(bvid).strip()
|
|
626
|
+
if not BV_RE.fullmatch(bvid):
|
|
627
|
+
raise ValueError("invalid bvid")
|
|
628
|
+
conn = self._connect()
|
|
629
|
+
try:
|
|
630
|
+
row = conn.execute("SELECT payload FROM subtitle_cache WHERE bvid=?", (bvid,)).fetchone()
|
|
631
|
+
if row is None or len(row["payload"].encode("utf-8")) > SUBTITLE_CACHE_BYTES:
|
|
632
|
+
return None
|
|
633
|
+
try:
|
|
634
|
+
payload = json.loads(row["payload"])
|
|
635
|
+
except (ValueError, TypeError):
|
|
636
|
+
return None
|
|
637
|
+
if (not isinstance(payload, dict) or payload.get("bvid") != bvid
|
|
638
|
+
or payload.get("error") or payload.get("message")
|
|
639
|
+
or not str(payload.get("text") or "").strip()):
|
|
640
|
+
return None
|
|
641
|
+
return payload
|
|
642
|
+
finally:
|
|
643
|
+
conn.close()
|
|
644
|
+
|
|
645
|
+
def reserve_subtitle(self, bvid):
|
|
646
|
+
"""Single-flight fetch lease independent of permanent seen/send reservations."""
|
|
647
|
+
bvid = str(bvid).strip()
|
|
648
|
+
if not BV_RE.fullmatch(bvid):
|
|
649
|
+
raise ValueError("invalid bvid")
|
|
650
|
+
now = self._clock()
|
|
651
|
+
with self._transaction() as conn:
|
|
652
|
+
self._import_legacy(conn)
|
|
653
|
+
legacy = conn.execute("SELECT * FROM guard_entries WHERE key=? AND kind='read'",
|
|
654
|
+
("seen:" + bvid,)).fetchone()
|
|
655
|
+
if legacy is not None and legacy["status"] == "pending" and (legacy["expires"] or 0) > now:
|
|
656
|
+
return {"allowed": False, "status": "pending", "kind": "read",
|
|
657
|
+
"retryAt": _iso(legacy["expires"]), "at": _iso(now)}
|
|
658
|
+
row = conn.execute("SELECT * FROM subtitle_reads WHERE bvid=?", (bvid,)).fetchone()
|
|
659
|
+
if row is not None and row["expires"] > now:
|
|
660
|
+
return {"allowed": False, "status": "pending", "kind": "read",
|
|
661
|
+
"retryAt": _iso(row["expires"]), "at": _iso(now)}
|
|
662
|
+
token = uuid.uuid4().hex
|
|
663
|
+
conn.execute("INSERT INTO subtitle_reads(bvid,token,expires) VALUES (?,?,?) "
|
|
664
|
+
"ON CONFLICT(bvid) DO UPDATE SET token=excluded.token,expires=excluded.expires",
|
|
665
|
+
(bvid, token, now + self.read_lease_seconds))
|
|
666
|
+
return {"allowed": True, "kind": "read", "subtitle": True, "bvid": bvid, "token": token}
|
|
667
|
+
|
|
668
|
+
def commit_subtitle(self, reservation, payload):
|
|
669
|
+
"""Publish cache + first-seen record atomically; eviction never removes history."""
|
|
670
|
+
bvid = str(reservation.get("bvid") or "")
|
|
671
|
+
if not reservation.get("subtitle") or not BV_RE.fullmatch(bvid):
|
|
672
|
+
raise ValueError("invalid subtitle reservation")
|
|
673
|
+
if (not isinstance(payload, dict) or payload.get("bvid") != bvid
|
|
674
|
+
or payload.get("error") or payload.get("message")
|
|
675
|
+
or not str(payload.get("text") or "").strip()):
|
|
676
|
+
raise ValueError("subtitle content required")
|
|
677
|
+
packed = json.dumps(payload, ensure_ascii=False)
|
|
678
|
+
now = self._clock()
|
|
679
|
+
with self._transaction() as conn:
|
|
680
|
+
changed = conn.execute("DELETE FROM subtitle_reads WHERE bvid=? AND token=?",
|
|
681
|
+
(bvid, reservation["token"])).rowcount
|
|
682
|
+
if changed != 1:
|
|
683
|
+
raise RuntimeError("subtitle fetch lease was lost")
|
|
684
|
+
if len(packed.encode("utf-8")) <= SUBTITLE_CACHE_BYTES:
|
|
685
|
+
conn.execute("INSERT INTO subtitle_cache(bvid,fetched,payload) VALUES (?,?,?) "
|
|
686
|
+
"ON CONFLICT(bvid) DO UPDATE SET fetched=excluded.fetched,payload=excluded.payload",
|
|
687
|
+
(bvid, now, packed))
|
|
688
|
+
conn.execute("DELETE FROM subtitle_cache WHERE bvid NOT IN "
|
|
689
|
+
"(SELECT bvid FROM subtitle_cache ORDER BY fetched DESC,bvid LIMIT ?)",
|
|
690
|
+
(SUBTITLE_CACHE_MAX,))
|
|
691
|
+
# INSERT OR IGNORE keeps the original first-seen timestamp and its provenance.
|
|
692
|
+
conn.execute("DELETE FROM guard_entries WHERE key=? AND kind='read' AND status='pending' AND expires<=?",
|
|
693
|
+
("seen:" + bvid, now))
|
|
694
|
+
self._import_one(conn, "seen:" + bvid, "read",
|
|
695
|
+
{"bvid": bvid, "title": payload.get("title", ""), "at": _iso(now)})
|
|
696
|
+
return len(packed.encode("utf-8")) <= SUBTITLE_CACHE_BYTES
|
|
697
|
+
|
|
597
698
|
def reserve_send(self, key, legacy_keys=()):
|
|
598
699
|
if not key:
|
|
599
700
|
raise ValueError("empty send key")
|
|
@@ -630,6 +731,9 @@ class RuntimeGuard:
|
|
|
630
731
|
def release(self, reservation):
|
|
631
732
|
"""Only use for failed reads or a send proven not to have started."""
|
|
632
733
|
with self._transaction() as conn:
|
|
734
|
+
if reservation.get("subtitle"):
|
|
735
|
+
return conn.execute("DELETE FROM subtitle_reads WHERE bvid=? AND token=?",
|
|
736
|
+
(reservation["bvid"], reservation["token"])).rowcount == 1
|
|
633
737
|
changed = conn.execute("DELETE FROM guard_entries WHERE key=? AND token=? AND status='pending'",
|
|
634
738
|
(reservation["key"], reservation["token"])).rowcount == 1
|
|
635
739
|
if changed:
|
|
@@ -710,6 +814,9 @@ def guard_snapshot(home=None, bvids=None, limit=200):
|
|
|
710
814
|
result["recentSends"] = [_public(row) for row in conn.execute(
|
|
711
815
|
"SELECT * FROM guard_entries WHERE kind='send' ORDER BY updated DESC LIMIT ?", (limit,))]
|
|
712
816
|
tables = {row["name"] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
817
|
+
if "subtitle_reads" in tables:
|
|
818
|
+
leases = conn.execute("SELECT bvid FROM subtitle_reads WHERE expires>? LIMIT ?", (now, limit)).fetchall()
|
|
819
|
+
result["activeReads"] = list(dict.fromkeys(result["activeReads"] + [row["bvid"] for row in leases]))[:limit]
|
|
713
820
|
if {"guard_pace_events", "guard_pace_state"} <= tables:
|
|
714
821
|
result["pace"] = {action: RuntimeGuard._pace_check(conn, action, now) for action in PACE}
|
|
715
822
|
finally:
|