dsh-biocore 0.2.0 → 0.3.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/README.md +11 -0
- package/lib/core-worker.js +268 -0
- package/lib/index.js +160 -70
- package/lib/modules/autonomy.js +125 -0
- package/lib/modules/cache-stats.js +48 -0
- package/lib/modules/core-turn.js +24 -6
- package/lib/modules/expression-inject.js +0 -10
- package/lib/modules/expression-refresh.js +0 -9
- package/lib/modules/memory-capture.js +2 -1
- package/lib/modules/memory-inject.js +50 -5
- package/lib/modules/memory-tools.js +7 -4
- package/lib/modules/python-env.js +46 -0
- package/lib/modules/safety-gate.js +0 -3
- package/lib/modules/series.js +54 -0
- package/lib/modules/switch.js +16 -0
- package/lib/py-bridge.js +81 -15
- package/package.json +1 -1
- package/python/biocore_memory/core/config.py +138 -127
- package/python/biocore_memory/engine.py +2653 -2476
- package/python/biocore_memory/recall/embedding.py +129 -0
- package/python/biocore_memory/recall/feedback.py +202 -0
- package/python/biocore_model/preference_drift.py +255 -0
- package/python/biocore_model/tests/__init__.py +1 -0
- package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
- package/python/biocore_model/tests/core_status_check.mjs +29 -0
- package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
- package/python/biocore_model/tests/leak_samples_run.py +18 -0
- package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
- package/python/biocore_model/tests/panel_html_check.mjs +63 -0
- package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
- package/python/biocore_model/tests/render_guard_check.mjs +19 -0
- package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
- package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
- package/python/biocore_model/tests/test_affective_labor.py +102 -0
- package/python/biocore_model/tests/test_appraisal.py +137 -0
- package/python/biocore_model/tests/test_appraisal_action.py +55 -0
- package/python/biocore_model/tests/test_arch_convergence.py +35 -0
- package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
- package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
- package/python/biocore_model/tests/test_attachment_state.py +29 -0
- package/python/biocore_model/tests/test_attribution.py +56 -0
- package/python/biocore_model/tests/test_attributor.py +108 -0
- package/python/biocore_model/tests/test_autonomic.py +69 -0
- package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
- package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
- package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
- package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
- package/python/biocore_model/tests/test_autonomy_check.py +40 -0
- package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
- package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
- package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
- package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
- package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
- package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
- package/python/biocore_model/tests/test_autonomy_model.py +60 -0
- package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
- package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
- package/python/biocore_model/tests/test_autonomy_params.py +163 -0
- package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
- package/python/biocore_model/tests/test_autonomy_self.py +153 -0
- package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
- package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
- package/python/biocore_model/tests/test_autonomy_status.py +35 -0
- package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
- package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
- package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
- package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
- package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
- package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
- package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
- package/python/biocore_model/tests/test_bio_tick.py +202 -0
- package/python/biocore_model/tests/test_boundary.py +183 -0
- package/python/biocore_model/tests/test_boundary_gate.py +295 -0
- package/python/biocore_model/tests/test_brain_split.py +99 -0
- package/python/biocore_model/tests/test_co_recall.py +350 -0
- package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
- package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
- package/python/biocore_model/tests/test_connection_bids.py +69 -0
- package/python/biocore_model/tests/test_core_bridge.py +88 -0
- package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
- package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
- package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
- package/python/biocore_model/tests/test_core_client.py +156 -0
- package/python/biocore_model/tests/test_core_commands.py +146 -0
- package/python/biocore_model/tests/test_core_mode.py +161 -0
- package/python/biocore_model/tests/test_core_state.py +240 -0
- package/python/biocore_model/tests/test_criticality.py +61 -0
- package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
- package/python/biocore_model/tests/test_debug_values.py +169 -0
- package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
- package/python/biocore_model/tests/test_delta_budget.py +157 -0
- package/python/biocore_model/tests/test_diag_trace.py +201 -0
- package/python/biocore_model/tests/test_directives_compat.py +698 -0
- package/python/biocore_model/tests/test_dissonance.py +97 -0
- package/python/biocore_model/tests/test_drift_sign.py +87 -0
- package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
- package/python/biocore_model/tests/test_embedding_local.py +279 -0
- package/python/biocore_model/tests/test_embedding_mode.py +175 -0
- package/python/biocore_model/tests/test_embedding_modes.py +342 -0
- package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
- package/python/biocore_model/tests/test_emotion_probe.py +214 -0
- package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
- package/python/biocore_model/tests/test_entity_graph.py +198 -0
- package/python/biocore_model/tests/test_entity_resolve.py +446 -0
- package/python/biocore_model/tests/test_envy.py +173 -0
- package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
- package/python/biocore_model/tests/test_expr_guard.py +1 -0
- package/python/biocore_model/tests/test_expr_intent.py +66 -0
- package/python/biocore_model/tests/test_expr_state.py +66 -0
- package/python/biocore_model/tests/test_expr_template.py +71 -0
- package/python/biocore_model/tests/test_expr_tone.py +41 -0
- package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
- package/python/biocore_model/tests/test_feedback_flow.py +185 -0
- package/python/biocore_model/tests/test_feedback_guard.py +65 -0
- package/python/biocore_model/tests/test_feedback_llm.py +59 -0
- package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
- package/python/biocore_model/tests/test_feedback_memory.py +63 -0
- package/python/biocore_model/tests/test_feedback_user.py +61 -0
- package/python/biocore_model/tests/test_frustration_curve.py +66 -0
- package/python/biocore_model/tests/test_global_bio_state.py +116 -0
- package/python/biocore_model/tests/test_governance.py +181 -0
- package/python/biocore_model/tests/test_granularity.py +133 -0
- package/python/biocore_model/tests/test_graph_extract.py +224 -0
- package/python/biocore_model/tests/test_graph_recall.py +226 -0
- package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
- package/python/biocore_model/tests/test_growth.py +301 -0
- package/python/biocore_model/tests/test_intent_render.py +60 -0
- package/python/biocore_model/tests/test_intero_regulation.py +224 -0
- package/python/biocore_model/tests/test_intero_style.py +146 -0
- package/python/biocore_model/tests/test_item3_numeric.py +105 -0
- package/python/biocore_model/tests/test_item6_numeric.py +116 -0
- package/python/biocore_model/tests/test_layered_memory.py +174 -0
- package/python/biocore_model/tests/test_leak_guard.py +75 -0
- package/python/biocore_model/tests/test_learning_probe.py +93 -0
- package/python/biocore_model/tests/test_life_loop.py +222 -0
- package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
- package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
- package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
- package/python/biocore_model/tests/test_memory_backend.py +332 -0
- package/python/biocore_model/tests/test_memory_dedup.py +316 -0
- package/python/biocore_model/tests/test_memory_dual.py +219 -0
- package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
- package/python/biocore_model/tests/test_memory_feedback.py +241 -0
- package/python/biocore_model/tests/test_memory_fixes.py +84 -0
- package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
- package/python/biocore_model/tests/test_memory_inject.py +211 -0
- package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
- package/python/biocore_model/tests/test_memory_store.py +372 -0
- package/python/biocore_model/tests/test_metacog.py +103 -0
- package/python/biocore_model/tests/test_migrate.py +245 -0
- package/python/biocore_model/tests/test_migrate_store.py +194 -0
- package/python/biocore_model/tests/test_mind_wander.py +65 -0
- package/python/biocore_model/tests/test_mood.py +35 -0
- package/python/biocore_model/tests/test_narrative.py +114 -0
- package/python/biocore_model/tests/test_needs_esteem.py +51 -0
- package/python/biocore_model/tests/test_needs_events.py +205 -0
- package/python/biocore_model/tests/test_needs_sleep.py +54 -0
- package/python/biocore_model/tests/test_noise_bank.py +54 -0
- package/python/biocore_model/tests/test_numeric_schema.py +98 -0
- package/python/biocore_model/tests/test_output_protocol.py +65 -0
- package/python/biocore_model/tests/test_page_debug.py +147 -0
- package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
- package/python/biocore_model/tests/test_pipeline.py +345 -0
- package/python/biocore_model/tests/test_pipeline_api.py +192 -0
- package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
- package/python/biocore_model/tests/test_plugin_config.py +85 -0
- package/python/biocore_model/tests/test_policy_learning.py +116 -0
- package/python/biocore_model/tests/test_possible_selves.py +88 -0
- package/python/biocore_model/tests/test_predictor.py +227 -0
- package/python/biocore_model/tests/test_preference_drift.py +142 -0
- package/python/biocore_model/tests/test_preset.py +142 -0
- package/python/biocore_model/tests/test_proactive_context.py +70 -0
- package/python/biocore_model/tests/test_proactive_history.py +90 -0
- package/python/biocore_model/tests/test_proactive_memory.py +141 -0
- package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
- package/python/biocore_model/tests/test_probe_contract.py +71 -0
- package/python/biocore_model/tests/test_prospective.py +154 -0
- package/python/biocore_model/tests/test_provider_fallback.py +89 -0
- package/python/biocore_model/tests/test_reconstruction.py +111 -0
- package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
- package/python/biocore_model/tests/test_relation_stage.py +114 -0
- package/python/biocore_model/tests/test_relationship_stage.py +59 -0
- package/python/biocore_model/tests/test_release_140.py +55 -0
- package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
- package/python/biocore_model/tests/test_reply_limit.py +86 -0
- package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
- package/python/biocore_model/tests/test_reply_split.py +52 -0
- package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
- package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
- package/python/biocore_model/tests/test_rerank.py +229 -0
- package/python/biocore_model/tests/test_rollback.py +175 -0
- package/python/biocore_model/tests/test_runtime.py +175 -0
- package/python/biocore_model/tests/test_runtime_active.py +205 -0
- package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
- package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
- package/python/biocore_model/tests/test_scheduler.py +79 -0
- package/python/biocore_model/tests/test_secrecy.py +105 -0
- package/python/biocore_model/tests/test_self_esteem.py +45 -0
- package/python/biocore_model/tests/test_self_intent.py +347 -0
- package/python/biocore_model/tests/test_self_interest.py +183 -0
- package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
- package/python/biocore_model/tests/test_self_verification.py +113 -0
- package/python/biocore_model/tests/test_situation.py +142 -0
- package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
- package/python/biocore_model/tests/test_social_pain.py +49 -0
- package/python/biocore_model/tests/test_social_repair.py +67 -0
- package/python/biocore_model/tests/test_social_theory.py +78 -0
- package/python/biocore_model/tests/test_source_monitoring.py +204 -0
- package/python/biocore_model/tests/test_speak_like_group.py +36 -0
- package/python/biocore_model/tests/test_stages.py +226 -0
- package/python/biocore_model/tests/test_state_tag.py +86 -0
- package/python/biocore_model/tests/test_template_expr.py +65 -0
- package/python/biocore_model/tests/test_text_inference.py +70 -0
- package/python/biocore_model/tests/test_theta_params.py +115 -0
- package/python/biocore_model/tests/test_topic_control.py +200 -0
- package/python/biocore_model/tests/test_turn_memory.py +105 -0
- package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
- package/python/biocore_model/tests/test_ui_settings.py +84 -0
- package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
- package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
- package/python/biocore_model/tests/test_user_classify.py +122 -0
- package/python/biocore_model/tests/test_value_order.py +117 -0
- package/python/biocore_model/tests/test_voice_bank.py +105 -0
- package/python/biocore_model/tests/test_waiting_sense.py +216 -0
- package/python/biocore_model/tests/test_x_directives.py +101 -0
- package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
- package/python/biocore_model/tests/view_mode_check.mjs +17 -0
- package/python/biocore_runtime/autonomy.py +208 -0
- package/python/biocore_runtime/core_state.py +302 -0
- package/python/biocore_runtime/governance.py +495 -0
- package/python/biocore_runtime/growth.py +528 -0
- package/python/biocore_runtime/pipeline.py +850 -23
- package/python/biocore_runtime/relationship_state.py +214 -0
- package/python/biocore_runtime/self_layer.py +103 -0
- package/python/biocore_runtime/service.py +162 -0
- package/python/biocore_runtime/standby.py +472 -0
- package/ui/panel-layout.js +83 -39
- package/ui/panel.css +885 -743
- package/ui/panel.html +356 -296
- package/ui/panel.js +739 -865
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Standby initiative mode: when idle, let a small model write one line.
|
|
3
|
+
|
|
4
|
+
Config (dict or BIOCORE_STANDBY_* env; every key optional):
|
|
5
|
+
standby_enabled False
|
|
6
|
+
standby_model_base_url ""
|
|
7
|
+
standby_model ""
|
|
8
|
+
standby_model_key ""
|
|
9
|
+
standby_every_minutes 30
|
|
10
|
+
standby_active_hours "09:00-23:00"
|
|
11
|
+
standby_max_per_day 6
|
|
12
|
+
standby_channel "outbox"
|
|
13
|
+
standby_timeout 20
|
|
14
|
+
standby_store "" (memory store root; defaults to plugin data/memory)
|
|
15
|
+
standby_session_id "standby"
|
|
16
|
+
|
|
17
|
+
tick(now) reads the same session state as turn_start, asks the existing
|
|
18
|
+
pipeline.proactive_tick for the decision, then (only if should_send) builds a
|
|
19
|
+
full-persona prompt and calls an OpenAI-compatible /v1/chat/completions.
|
|
20
|
+
A successful line is sanitized and appended to
|
|
21
|
+
<store>/proactive/outbox.jsonl; cooldown/daily bookkeeping goes to
|
|
22
|
+
<store>/proactive/state.json. Any model failure writes no outbox.
|
|
23
|
+
|
|
24
|
+
CLI: python biocore_runtime/standby.py tick
|
|
25
|
+
python biocore_runtime/standby.py sleep-loop [--once]
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import math
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
import urllib.error
|
|
35
|
+
import urllib.request
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
_PLUGIN_ROOT = str(Path(__file__).resolve().parents[1])
|
|
39
|
+
if _PLUGIN_ROOT not in sys.path:
|
|
40
|
+
sys.path.insert(0, _PLUGIN_ROOT)
|
|
41
|
+
|
|
42
|
+
from biocore_runtime import pipeline as _pipeline # noqa: E402
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from biocore_model import sanitize as _sanitize
|
|
46
|
+
except Exception: # pragma: no cover
|
|
47
|
+
_sanitize = None
|
|
48
|
+
|
|
49
|
+
DEFAULTS = {
|
|
50
|
+
"standby_enabled": False,
|
|
51
|
+
"standby_model_base_url": "",
|
|
52
|
+
"standby_model": "",
|
|
53
|
+
"standby_model_key": "",
|
|
54
|
+
"standby_every_minutes": 30,
|
|
55
|
+
"standby_active_hours": "09:00-23:00",
|
|
56
|
+
"standby_max_per_day": 6,
|
|
57
|
+
"standby_channel": "outbox",
|
|
58
|
+
"standby_timeout": 20.0,
|
|
59
|
+
"standby_store": "",
|
|
60
|
+
"standby_session_id": "standby",
|
|
61
|
+
"standby_min_trust": 0.6,
|
|
62
|
+
"standby_require_open_loop": True,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _num(value, default=0.0):
|
|
67
|
+
try:
|
|
68
|
+
number = float(value)
|
|
69
|
+
except Exception:
|
|
70
|
+
return float(default)
|
|
71
|
+
return number if math.isfinite(number) else float(default)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _as_bool(value, default=False):
|
|
75
|
+
if isinstance(value, bool):
|
|
76
|
+
return value
|
|
77
|
+
if isinstance(value, (int, float)):
|
|
78
|
+
return bool(value)
|
|
79
|
+
if isinstance(value, str):
|
|
80
|
+
lowered = value.strip().lower()
|
|
81
|
+
if lowered in ("1", "true", "yes", "on"):
|
|
82
|
+
return True
|
|
83
|
+
if lowered in ("0", "false", "no", "off", ""):
|
|
84
|
+
return False
|
|
85
|
+
return bool(default)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def normalize_config(config=None):
|
|
89
|
+
data = config if isinstance(config, dict) else {}
|
|
90
|
+
out = dict(DEFAULTS)
|
|
91
|
+
for key in DEFAULTS:
|
|
92
|
+
env_key = "BIOCORE_" + key.upper()
|
|
93
|
+
if key in data:
|
|
94
|
+
out[key] = data[key]
|
|
95
|
+
elif os.environ.get(env_key) is not None:
|
|
96
|
+
out[key] = os.environ.get(env_key)
|
|
97
|
+
out["standby_enabled"] = _as_bool(out["standby_enabled"], False)
|
|
98
|
+
for key in ("standby_model_base_url", "standby_model",
|
|
99
|
+
"standby_model_key", "standby_channel", "standby_store",
|
|
100
|
+
"standby_session_id"):
|
|
101
|
+
out[key] = str(out.get(key) or "").strip()
|
|
102
|
+
out["standby_every_minutes"] = max(
|
|
103
|
+
1, min(24 * 60, int(_num(out.get("standby_every_minutes"), 30))))
|
|
104
|
+
out["standby_max_per_day"] = max(
|
|
105
|
+
1, min(100, int(_num(out.get("standby_max_per_day"), 6))))
|
|
106
|
+
out["standby_timeout"] = max(
|
|
107
|
+
0.2, min(120.0, _num(out.get("standby_timeout"), 20.0)))
|
|
108
|
+
out["standby_min_trust"] = max(
|
|
109
|
+
0.0, min(1.0, _num(out.get("standby_min_trust"), 0.6)))
|
|
110
|
+
out["standby_require_open_loop"] = _as_bool(
|
|
111
|
+
out.get("standby_require_open_loop"), True)
|
|
112
|
+
out["standby_session_id"] = out["standby_session_id"] or "standby"
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _store_root(store):
|
|
117
|
+
raw = str(store or "").strip()
|
|
118
|
+
if raw:
|
|
119
|
+
return Path(raw)
|
|
120
|
+
return Path(_pipeline._default_store())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _proactive_dir(store):
|
|
124
|
+
return _store_root(store) / "proactive"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _outbox_path(store):
|
|
128
|
+
return _proactive_dir(store) / "outbox.jsonl"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _bookkeeping_path(store):
|
|
132
|
+
return _proactive_dir(store) / "state.json"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_json(path, fallback):
|
|
136
|
+
try:
|
|
137
|
+
if path is None or not Path(path).exists():
|
|
138
|
+
return fallback
|
|
139
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
140
|
+
return data if isinstance(data, dict) else fallback
|
|
141
|
+
except Exception:
|
|
142
|
+
return fallback
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _write_json(path, data):
|
|
146
|
+
path = Path(path)
|
|
147
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
149
|
+
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2),
|
|
150
|
+
encoding="utf-8")
|
|
151
|
+
os.replace(tmp, path)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _in_active_hours(now, spec):
|
|
155
|
+
text = str(spec or "").strip()
|
|
156
|
+
if not text:
|
|
157
|
+
return True
|
|
158
|
+
try:
|
|
159
|
+
start_text, end_text = text.split("-", 1)
|
|
160
|
+
start_h, start_m = [int(part) for part in start_text.split(":")]
|
|
161
|
+
end_h, end_m = [int(part) for part in end_text.split(":")]
|
|
162
|
+
except Exception:
|
|
163
|
+
return True
|
|
164
|
+
local = time.localtime(now)
|
|
165
|
+
minutes = local.tm_hour * 60 + local.tm_min
|
|
166
|
+
start = start_h * 60 + start_m
|
|
167
|
+
end = end_h * 60 + end_m
|
|
168
|
+
if start == end:
|
|
169
|
+
return True
|
|
170
|
+
if start < end:
|
|
171
|
+
return start <= minutes < end
|
|
172
|
+
return minutes >= start or minutes < end
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _day_key(now):
|
|
176
|
+
return time.strftime("%Y-%m-%d", time.localtime(now))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _sent_today(state, now):
|
|
180
|
+
sent = state.get("sent") if isinstance(state.get("sent"), list) else []
|
|
181
|
+
today = _day_key(now)
|
|
182
|
+
values = []
|
|
183
|
+
for item in sent:
|
|
184
|
+
stamp = _num(item, 0.0)
|
|
185
|
+
if stamp > 0 and time.strftime("%Y-%m-%d", time.localtime(stamp)) == today:
|
|
186
|
+
values.append(stamp)
|
|
187
|
+
return values
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def load_state(store):
|
|
191
|
+
return _load_json(_bookkeeping_path(store), {})
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def load_session_state(store, session_id):
|
|
195
|
+
state = _pipeline._load_session_state(_store_root(store), session_id)
|
|
196
|
+
if not isinstance(state, dict):
|
|
197
|
+
state = {}
|
|
198
|
+
return _pipeline.apply_intero_to_state(state)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build_prompt(state, memories, persona_text="", channel="outbox"):
|
|
202
|
+
"""Full persona (never truncated) + thinking rule + state + recent memories."""
|
|
203
|
+
parts = []
|
|
204
|
+
if persona_text:
|
|
205
|
+
parts.append(str(persona_text))
|
|
206
|
+
parts.append(_pipeline.THINKING_STYLE_TEXT)
|
|
207
|
+
try:
|
|
208
|
+
state_line = _pipeline._state_block(state, 200)
|
|
209
|
+
except Exception:
|
|
210
|
+
state_line = "此刻:心情平稳。"
|
|
211
|
+
parts.append(state_line)
|
|
212
|
+
recent = [str(item).strip() for item in (memories or []) if str(item).strip()]
|
|
213
|
+
if recent:
|
|
214
|
+
parts.append("最近记得:" + ";".join(recent[:3]))
|
|
215
|
+
prompt = "\n\n".join(parts)
|
|
216
|
+
return prompt
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _recent_memories(store, session_id, limit=3):
|
|
220
|
+
try:
|
|
221
|
+
doc = _pipeline.load_memory(_store_root(store), session_id)
|
|
222
|
+
entries = [entry for entry in (doc.get("entries") or [])
|
|
223
|
+
if isinstance(entry, dict) and not entry.get("deleted")
|
|
224
|
+
and not entry.get("archived")]
|
|
225
|
+
entries.sort(key=lambda entry: _num(entry.get("ts"), 0.0), reverse=True)
|
|
226
|
+
return [str(entry.get("text") or "")[:120] for entry in entries[:limit]]
|
|
227
|
+
except Exception:
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _persona_text(store):
|
|
232
|
+
try:
|
|
233
|
+
profile = _pipeline.load_persona_profile({"store": str(_store_root(store))})
|
|
234
|
+
return str((profile or {}).get("persona_text") or "")
|
|
235
|
+
except Exception:
|
|
236
|
+
return ""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _chat_url(base_url):
|
|
240
|
+
base = str(base_url or "").strip().rstrip("/")
|
|
241
|
+
if not base:
|
|
242
|
+
return ""
|
|
243
|
+
if base.endswith("/v1"):
|
|
244
|
+
return base + "/chat/completions"
|
|
245
|
+
return base + "/v1/chat/completions"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _urllib_transport(url, payload, timeout, headers):
|
|
249
|
+
request = urllib.request.Request(
|
|
250
|
+
url, data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
|
251
|
+
headers=headers, method="POST")
|
|
252
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
253
|
+
status = int(getattr(response, "status", 200) or 200)
|
|
254
|
+
body = response.read()
|
|
255
|
+
return status, body
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _extract_text(response_body):
|
|
259
|
+
try:
|
|
260
|
+
if isinstance(response_body, bytes):
|
|
261
|
+
response_body = response_body.decode("utf-8", "replace")
|
|
262
|
+
data = json.loads(response_body)
|
|
263
|
+
choices = data.get("choices") if isinstance(data, dict) else None
|
|
264
|
+
if not isinstance(choices, list) or not choices:
|
|
265
|
+
return ""
|
|
266
|
+
message = choices[0].get("message") if isinstance(choices[0], dict) else {}
|
|
267
|
+
return str((message or {}).get("content") or "").strip()
|
|
268
|
+
except Exception:
|
|
269
|
+
return ""
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def generate(prompt, config, transport=None):
|
|
273
|
+
"""Call the OpenAI-compatible chat endpoint; return (text, reason)."""
|
|
274
|
+
cfg = normalize_config(config)
|
|
275
|
+
base = cfg["standby_model_base_url"]
|
|
276
|
+
model = cfg["standby_model"]
|
|
277
|
+
if not base or not model:
|
|
278
|
+
return "", "missing_config"
|
|
279
|
+
url = _chat_url(base)
|
|
280
|
+
if not url:
|
|
281
|
+
return "", "missing_config"
|
|
282
|
+
payload = {
|
|
283
|
+
"model": model,
|
|
284
|
+
"messages": [
|
|
285
|
+
{"role": "system", "content": prompt},
|
|
286
|
+
{"role": "user", "content": "现在没有人说话。你想主动发一句,只回一句话,不超过60字。"},
|
|
287
|
+
],
|
|
288
|
+
"temperature": 0.8,
|
|
289
|
+
"max_tokens": 120,
|
|
290
|
+
}
|
|
291
|
+
headers = {"Content-Type": "application/json"}
|
|
292
|
+
if cfg["standby_model_key"]:
|
|
293
|
+
headers["Authorization"] = "Bearer " + cfg["standby_model_key"]
|
|
294
|
+
call = transport or _urllib_transport
|
|
295
|
+
try:
|
|
296
|
+
status, body = call(url, payload, cfg["standby_timeout"], headers)
|
|
297
|
+
except (TimeoutError, OSError) as error:
|
|
298
|
+
return "", "timeout" if isinstance(error, TimeoutError) else "model_error"
|
|
299
|
+
except Exception:
|
|
300
|
+
return "", "model_error"
|
|
301
|
+
try:
|
|
302
|
+
status = int(status)
|
|
303
|
+
except Exception:
|
|
304
|
+
status = 0
|
|
305
|
+
if status < 200 or status >= 300:
|
|
306
|
+
return "", "http_%s" % status
|
|
307
|
+
text = _extract_text(body)
|
|
308
|
+
if not text:
|
|
309
|
+
return "", "empty_reply"
|
|
310
|
+
text = text.replace("\n", " ").strip()
|
|
311
|
+
if len(text) > 60:
|
|
312
|
+
text = text[:59].rstrip() + "…"
|
|
313
|
+
if _sanitize is not None:
|
|
314
|
+
try:
|
|
315
|
+
text = _sanitize.clean(text)
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
if not str(text).strip():
|
|
319
|
+
return "", "empty_reply"
|
|
320
|
+
return str(text).strip(), "ok"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _bookkeep(store, now, state, sent=False, reason="", text=""):
|
|
324
|
+
path = _bookkeeping_path(store)
|
|
325
|
+
sent_list = [stamp for stamp in (state.get("sent") or [])
|
|
326
|
+
if _num(stamp, 0.0) > 0 and now - _num(stamp, 0.0) < 7 * 86400]
|
|
327
|
+
if sent:
|
|
328
|
+
sent_list.append(now)
|
|
329
|
+
data = {
|
|
330
|
+
"last_attempt_ts": now,
|
|
331
|
+
"last_sent_ts": now if sent else _num(state.get("last_sent_ts"), 0.0),
|
|
332
|
+
"sent": sent_list[-50:],
|
|
333
|
+
"last_reason": reason,
|
|
334
|
+
"last_text": text[:80],
|
|
335
|
+
"ts": now,
|
|
336
|
+
}
|
|
337
|
+
try:
|
|
338
|
+
_write_json(path, data)
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
return data
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def tick(now=None, config=None, transport=None, proactive=None):
|
|
345
|
+
"""One standby tick. Returns {ok, sent, reason, text?, channel, outbox?}."""
|
|
346
|
+
cfg = normalize_config(config)
|
|
347
|
+
store = cfg["standby_store"] or _pipeline._default_store()
|
|
348
|
+
session_id = cfg["standby_session_id"]
|
|
349
|
+
now_value = _num(now, time.time())
|
|
350
|
+
if now_value > 1e12:
|
|
351
|
+
now_value = now_value / 1000.0
|
|
352
|
+
result = {"ok": True, "sent": False, "reason": "",
|
|
353
|
+
"channel": cfg["standby_channel"]}
|
|
354
|
+
if not cfg["standby_enabled"]:
|
|
355
|
+
result["reason"] = "disabled"
|
|
356
|
+
return result
|
|
357
|
+
if not _in_active_hours(now_value, cfg["standby_active_hours"]):
|
|
358
|
+
result["reason"] = "outside_active_hours"
|
|
359
|
+
return result
|
|
360
|
+
state = load_state(store)
|
|
361
|
+
if len(_sent_today(state, now_value)) >= cfg["standby_max_per_day"]:
|
|
362
|
+
result["reason"] = "daily_cap"
|
|
363
|
+
return result
|
|
364
|
+
last_attempt = _num(state.get("last_attempt_ts"), 0.0)
|
|
365
|
+
last_sent = _num(state.get("last_sent_ts"), 0.0)
|
|
366
|
+
cooldown_until = max(last_attempt, last_sent) + cfg["standby_every_minutes"] * 60.0
|
|
367
|
+
if cooldown_until > now_value:
|
|
368
|
+
result["reason"] = "cooldown"
|
|
369
|
+
return result
|
|
370
|
+
session_state = load_session_state(store, session_id)
|
|
371
|
+
last_human = _num(session_state.get("last_human_ts"),
|
|
372
|
+
now_value - max(cfg["standby_every_minutes"], 180) * 60.0)
|
|
373
|
+
switches = {
|
|
374
|
+
"proactiveEnabled": True,
|
|
375
|
+
"proactiveSend": True,
|
|
376
|
+
"proactiveMinGapMinutes": cfg["standby_every_minutes"],
|
|
377
|
+
"proactivePerHour": 6,
|
|
378
|
+
"proactiveMinTrust": cfg["standby_min_trust"],
|
|
379
|
+
"proactiveMaxChars": 60,
|
|
380
|
+
"proactiveRequireOpenLoop": cfg["standby_require_open_loop"],
|
|
381
|
+
}
|
|
382
|
+
decision = proactive
|
|
383
|
+
if decision is None:
|
|
384
|
+
decision = _pipeline.proactive_tick
|
|
385
|
+
try:
|
|
386
|
+
decision_result = decision(
|
|
387
|
+
session_id=session_id, last_human_ts=last_human,
|
|
388
|
+
switches=switches, store=store, now=now_value * 1000.0,
|
|
389
|
+
state_file=str(_pipeline.state_path(store, session_id)))
|
|
390
|
+
except Exception:
|
|
391
|
+
result["reason"] = "decision_error"
|
|
392
|
+
return result
|
|
393
|
+
if not isinstance(decision_result, dict):
|
|
394
|
+
result["reason"] = "decision_error"
|
|
395
|
+
return result
|
|
396
|
+
if not decision_result.get("should_send"):
|
|
397
|
+
result["reason"] = str(decision_result.get("reason") or "should_not_send")
|
|
398
|
+
return result
|
|
399
|
+
memories = _recent_memories(store, session_id)
|
|
400
|
+
persona = _persona_text(store)
|
|
401
|
+
prompt = build_prompt(session_state, memories, persona,
|
|
402
|
+
cfg["standby_channel"])
|
|
403
|
+
text, reason = generate(prompt, cfg, transport=transport)
|
|
404
|
+
if reason != "ok" or not text:
|
|
405
|
+
_bookkeep(store, now_value, state, sent=False, reason=reason)
|
|
406
|
+
result["reason"] = reason
|
|
407
|
+
return result
|
|
408
|
+
outbox = _outbox_path(store)
|
|
409
|
+
try:
|
|
410
|
+
outbox.parent.mkdir(parents=True, exist_ok=True)
|
|
411
|
+
row = {
|
|
412
|
+
"ts": round(now_value, 3),
|
|
413
|
+
"text": text,
|
|
414
|
+
"reason": "ok",
|
|
415
|
+
"state_snapshot": session_state,
|
|
416
|
+
"channel": cfg["standby_channel"],
|
|
417
|
+
"model": cfg["standby_model"],
|
|
418
|
+
}
|
|
419
|
+
with outbox.open("a", encoding="utf-8") as handle:
|
|
420
|
+
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
421
|
+
except Exception:
|
|
422
|
+
_bookkeep(store, now_value, state, sent=False, reason="outbox_write_failed")
|
|
423
|
+
result["reason"] = "outbox_write_failed"
|
|
424
|
+
return result
|
|
425
|
+
data = _bookkeep(store, now_value, state, sent=True, reason="ok", text=text)
|
|
426
|
+
result.update({"sent": True, "reason": "ok", "text": text,
|
|
427
|
+
"outbox": str(outbox), "sent_today": len(_sent_today(data, now_value))})
|
|
428
|
+
return result
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def sleep_loop(config=None, once=False):
|
|
432
|
+
cfg = normalize_config(config)
|
|
433
|
+
interval = max(60, int(cfg["standby_every_minutes"] * 60))
|
|
434
|
+
while True:
|
|
435
|
+
try:
|
|
436
|
+
result = tick(config=cfg)
|
|
437
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
438
|
+
except Exception as error:
|
|
439
|
+
print(json.dumps({"ok": False, "reason": "tick_error",
|
|
440
|
+
"detail": str(error)[:200]}, ensure_ascii=False))
|
|
441
|
+
if once:
|
|
442
|
+
return 0
|
|
443
|
+
try:
|
|
444
|
+
time.sleep(interval)
|
|
445
|
+
except KeyboardInterrupt:
|
|
446
|
+
return 0
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def main(argv=None):
|
|
450
|
+
args = list(argv if argv is not None else sys.argv[1:])
|
|
451
|
+
action = args[0] if args else "tick"
|
|
452
|
+
config = {}
|
|
453
|
+
index = 1
|
|
454
|
+
while index < len(args):
|
|
455
|
+
if args[index] == "--once":
|
|
456
|
+
config["once"] = True
|
|
457
|
+
index += 1
|
|
458
|
+
else:
|
|
459
|
+
break
|
|
460
|
+
if action == "tick":
|
|
461
|
+
result = tick(config=config)
|
|
462
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
463
|
+
return 0 if result.get("ok") else 1
|
|
464
|
+
if action == "sleep-loop":
|
|
465
|
+
return sleep_loop(config=config, once=bool(config.get("once")))
|
|
466
|
+
print(json.dumps({"ok": False, "reason": "unknown_action",
|
|
467
|
+
"action": action}, ensure_ascii=False))
|
|
468
|
+
return 1
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
if __name__ == "__main__":
|
|
472
|
+
raise SystemExit(main())
|
package/ui/panel-layout.js
CHANGED
|
@@ -1,27 +1,75 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** BioCore panel layout model (v3 rewrite, pure logic, no DOM).
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* One source of truth for: card registry (id -> tab/label/span), tab groups,
|
|
4
|
+
* hide/restore, ordering and localStorage persistence. panel.js only wires DOM
|
|
5
|
+
* events; selftest.mjs exercises this file inside a bare vm sandbox.
|
|
6
|
+
*
|
|
7
|
+
* Classification rule (hard): a card belongs to exactly one tab, and a tab is
|
|
8
|
+
* either all controls (settings) or all data (dashboard/memory/relation) or all
|
|
9
|
+
* diagnostics (tools). Never mixed.
|
|
6
10
|
*/
|
|
7
11
|
(function (root) {
|
|
8
12
|
'use strict'
|
|
9
13
|
|
|
10
14
|
var STORAGE_KEY = 'biocore-panel-layout-v2'
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
'memory', 'memory-summary', 'metrics',
|
|
14
|
-
'persona', 'model', 'switches', 'expression', 'advanced',
|
|
15
|
-
'runtime', 'trace',
|
|
16
|
-
]
|
|
15
|
+
var RAIL_KEY = 'biocore-panel-rail-v1'
|
|
16
|
+
|
|
17
17
|
var TAB_ORDER = ['dashboard', 'memory', 'relation', 'settings', 'tools']
|
|
18
|
-
var
|
|
19
|
-
dashboard:
|
|
20
|
-
memory:
|
|
21
|
-
relation:
|
|
22
|
-
settings:
|
|
23
|
-
tools:
|
|
24
|
-
}
|
|
18
|
+
var TAB_LABELS = {
|
|
19
|
+
dashboard: '仪表盘',
|
|
20
|
+
memory: '记忆',
|
|
21
|
+
relation: '关系',
|
|
22
|
+
settings: '配置',
|
|
23
|
+
tools: '工具',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Card registry. span is the 12-column desktop width; h the desktop band. */
|
|
27
|
+
var CARD_META = [
|
|
28
|
+
{ id: 'status', tab: 'dashboard', label: '此刻', span: 6, h: 208 },
|
|
29
|
+
{ id: 'mood', tab: 'dashboard', label: '心情', span: 3, h: 208 },
|
|
30
|
+
{ id: 'tension', tab: 'dashboard', label: '张力', span: 3, h: 208 },
|
|
31
|
+
{ id: 'warmth', tab: 'dashboard', label: '暖', span: 3, h: 176 },
|
|
32
|
+
{ id: 'heaviness', tab: 'dashboard', label: '沉', span: 3, h: 176 },
|
|
33
|
+
{ id: 'clarity', tab: 'dashboard', label: '清', span: 3, h: 176 },
|
|
34
|
+
{ id: 'channels-avg', tab: 'dashboard', label: '四通道均值', span: 3, h: 176 },
|
|
35
|
+
{ id: 'chart', tab: 'dashboard', label: '24 小时曲线', span: 8, h: 200 },
|
|
36
|
+
{ id: 'expression-now', tab: 'dashboard', label: '此刻的表达', span: 4, h: 200 },
|
|
37
|
+
{ id: 'today', tab: 'dashboard', label: '今日统计', span: 12, h: 120 },
|
|
38
|
+
|
|
39
|
+
{ id: 'memory', tab: 'memory', label: '最近记忆', span: 8, h: 232 },
|
|
40
|
+
{ id: 'memory-summary', tab: 'memory', label: '记忆概览', span: 4, h: 232 },
|
|
41
|
+
|
|
42
|
+
{ id: 'metrics', tab: 'relation', label: '关系数值', span: 6, h: 176 },
|
|
43
|
+
{ id: 'narrative', tab: 'relation', label: '叙事摘要', span: 6, h: 176 },
|
|
44
|
+
|
|
45
|
+
{ id: 'persona', tab: 'settings', label: '人设', span: 6, h: 320 },
|
|
46
|
+
{ id: 'model', tab: 'settings', label: '模型与接口', span: 6, h: 320 },
|
|
47
|
+
{ id: 'switches', tab: 'settings', label: '开关', span: 4, h: 320 },
|
|
48
|
+
{ id: 'expression', tab: 'settings', label: '表达', span: 4, h: 320 },
|
|
49
|
+
{ id: 'advanced', tab: 'settings', label: '维护', span: 4, h: 320 },
|
|
50
|
+
|
|
51
|
+
{ id: 'trace', tab: 'tools', label: '这一轮为什么', span: 12, h: 280 },
|
|
52
|
+
{ id: 'runtime', tab: 'tools', label: '运行时', span: 4, h: 200 },
|
|
53
|
+
{ id: 'diagnostics', tab: 'tools', label: '缓存与诊断', span: 8, h: 200 },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
var DEFAULT_ORDER = CARD_META.map(function (card) { return card.id })
|
|
57
|
+
|
|
58
|
+
var META_BY_ID = {}
|
|
59
|
+
for (var i = 0; i < CARD_META.length; i += 1) META_BY_ID[CARD_META[i].id] = CARD_META[i]
|
|
60
|
+
|
|
61
|
+
var TAB_GROUPS = {}
|
|
62
|
+
for (var t = 0; t < TAB_ORDER.length; t += 1) TAB_GROUPS[TAB_ORDER[t]] = []
|
|
63
|
+
for (var m = 0; m < CARD_META.length; m += 1) TAB_GROUPS[CARD_META[m].tab].push(CARD_META[m].id)
|
|
64
|
+
|
|
65
|
+
function cardMeta(id) {
|
|
66
|
+
return META_BY_ID[id] || null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tabOf(id) {
|
|
70
|
+
var meta = META_BY_ID[id]
|
|
71
|
+
return meta ? meta.tab : ''
|
|
72
|
+
}
|
|
25
73
|
|
|
26
74
|
function defaultLayout() {
|
|
27
75
|
return { order: DEFAULT_ORDER.slice(), hidden: [] }
|
|
@@ -79,17 +127,11 @@
|
|
|
79
127
|
var seen = {}
|
|
80
128
|
for (var i = 0; i < ranked.length; i += 1) {
|
|
81
129
|
var id = ranked[i]
|
|
82
|
-
if (list.indexOf(id) >= 0 && !seen[id]) {
|
|
83
|
-
seen[id] = 1
|
|
84
|
-
out.push(id)
|
|
85
|
-
}
|
|
130
|
+
if (list.indexOf(id) >= 0 && !seen[id]) { seen[id] = 1; out.push(id) }
|
|
86
131
|
}
|
|
87
132
|
for (var j = 0; j < list.length; j += 1) {
|
|
88
133
|
var extra = list[j]
|
|
89
|
-
if (!seen[extra]) {
|
|
90
|
-
seen[extra] = 1
|
|
91
|
-
out.push(extra)
|
|
92
|
-
}
|
|
134
|
+
if (!seen[extra]) { seen[extra] = 1; out.push(extra) }
|
|
93
135
|
}
|
|
94
136
|
return out
|
|
95
137
|
}
|
|
@@ -121,15 +163,6 @@
|
|
|
121
163
|
return { order: normalized.order.slice(), hidden: hidden }
|
|
122
164
|
}
|
|
123
165
|
|
|
124
|
-
/** Pure pointer-intent: only edit mode drags, always via a long press
|
|
125
|
-
* (the card itself is the handle — no separate ⠿ control). */
|
|
126
|
-
function dragIntent(options) {
|
|
127
|
-
var opts = options || {}
|
|
128
|
-
if (!opts.editing) return { start: false, hold: false }
|
|
129
|
-
return { start: false, hold: true }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Re-show one hidden card (undo of hideCard). */
|
|
133
166
|
function showCard(layout, id) {
|
|
134
167
|
var normalized = normalizeLayout(layout)
|
|
135
168
|
if (normalized.hidden.indexOf(id) < 0) return normalized
|
|
@@ -137,7 +170,14 @@
|
|
|
137
170
|
return { order: normalized.order.slice(), hidden: hidden }
|
|
138
171
|
}
|
|
139
172
|
|
|
140
|
-
/**
|
|
173
|
+
/** Pointer intent: only edit mode drags; touch needs a long press first. */
|
|
174
|
+
function dragIntent(options) {
|
|
175
|
+
var opts = options || {}
|
|
176
|
+
if (!opts.editing) return { start: false, hold: false }
|
|
177
|
+
return { start: false, hold: true }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Cards belonging to one tab, in the given order. */
|
|
141
181
|
function cardsForTab(ids, tab) {
|
|
142
182
|
var list = Array.isArray(ids) ? ids : []
|
|
143
183
|
var group = TAB_GROUPS[tab] || TAB_GROUPS.dashboard
|
|
@@ -146,8 +186,7 @@
|
|
|
146
186
|
|
|
147
187
|
function readLayout(storage) {
|
|
148
188
|
try {
|
|
149
|
-
var raw = storage && typeof storage.getItem === 'function'
|
|
150
|
-
? storage.getItem(STORAGE_KEY) : null
|
|
189
|
+
var raw = storage && typeof storage.getItem === 'function' ? storage.getItem(STORAGE_KEY) : null
|
|
151
190
|
if (!raw) return defaultLayout()
|
|
152
191
|
return normalizeLayout(JSON.parse(raw))
|
|
153
192
|
} catch {
|
|
@@ -177,9 +216,14 @@
|
|
|
177
216
|
|
|
178
217
|
var api = {
|
|
179
218
|
STORAGE_KEY: STORAGE_KEY,
|
|
180
|
-
|
|
219
|
+
RAIL_KEY: RAIL_KEY,
|
|
181
220
|
TAB_ORDER: TAB_ORDER.slice(),
|
|
221
|
+
TAB_LABELS: TAB_LABELS,
|
|
182
222
|
TAB_GROUPS: TAB_GROUPS,
|
|
223
|
+
CARD_META: CARD_META.slice(),
|
|
224
|
+
DEFAULT_ORDER: DEFAULT_ORDER.slice(),
|
|
225
|
+
cardMeta: cardMeta,
|
|
226
|
+
tabOf: tabOf,
|
|
183
227
|
cardsForTab: cardsForTab,
|
|
184
228
|
defaultLayout: defaultLayout,
|
|
185
229
|
normalizeOrder: normalizeOrder,
|
|
@@ -196,4 +240,4 @@
|
|
|
196
240
|
}
|
|
197
241
|
root.BioCorePanelLayout = api
|
|
198
242
|
})(typeof window !== 'undefined' ? window
|
|
199
|
-
: (typeof globalThis !== 'undefined' ? globalThis : this));
|
|
243
|
+
: (typeof globalThis !== 'undefined' ? globalThis : this));
|