foxclaw 0.1.2 → 0.1.3
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/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { t } from '../../lib/i18n.js'
|
|
3
|
+
|
|
2
4
|
const props = defineProps({
|
|
3
5
|
title: { type: String, default: '' },
|
|
4
6
|
tag: { type: String, default: '' },
|
|
5
7
|
state: { type: String, default: 'idle' },
|
|
6
|
-
/* 已有旧数据时,刷新不要整块闪成「加载中」 */
|
|
7
8
|
ready: { type: Boolean, default: false },
|
|
8
9
|
})
|
|
9
10
|
defineEmits(['refresh'])
|
|
@@ -16,21 +17,19 @@ defineEmits(['refresh'])
|
|
|
16
17
|
<h2>{{ props.title }}</h2>
|
|
17
18
|
<span v-if="props.tag" class="pn-tag">{{ props.tag }}</span>
|
|
18
19
|
</div>
|
|
19
|
-
<button class="rf" :class="{ spin: props.state === 'loading' }" title="
|
|
20
|
-
|
|
20
|
+
<button class="rf" :class="{ spin: props.state === 'loading' }" :title="t('common.refresh')" @click="$emit('refresh')">
|
|
21
|
+
{{ t('common.refresh') }}
|
|
21
22
|
</button>
|
|
22
23
|
</header>
|
|
23
24
|
<div class="pb scroll">
|
|
24
|
-
<div v-if="props.state === 'loading' && !props.ready" class="pn-st"
|
|
25
|
-
<div v-else-if="props.state === 'error'" class="pn-st err"
|
|
25
|
+
<div v-if="props.state === 'loading' && !props.ready" class="pn-st">{{ t('common.loading') }}</div>
|
|
26
|
+
<div v-else-if="props.state === 'error'" class="pn-st err">{{ t('common.offlinePanel') }}</div>
|
|
26
27
|
<slot v-else />
|
|
27
28
|
</div>
|
|
28
29
|
</section>
|
|
29
30
|
</template>
|
|
30
31
|
|
|
31
32
|
<style scoped>
|
|
32
|
-
/* min-width: 0 让面板能被父级 flex 压窄 —— 否则 flex item 的 min-width:auto 会把它按
|
|
33
|
-
min-content 撑开,窗口拉到 420px 时面板比中栏还宽,右侧内容直接落到视口外。 */
|
|
34
33
|
.panel { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
|
35
34
|
.ph {
|
|
36
35
|
display: flex; align-items: center; gap: 10px; flex-shrink: 0;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
/* 复习面板 = SM-2 账本 + 今日队列 + 遗忘曲线。
|
|
3
|
-
卡片就是知识库里的 KU(复习状态在 KU.extra.review 里),不另存一份。 */
|
|
4
2
|
import { computed, onMounted, ref } from 'vue'
|
|
5
3
|
import PanelBox from './PanelBox.vue'
|
|
6
4
|
import {
|
|
@@ -20,10 +18,6 @@ const pos = computed(() => Math.min(rv.index + 1, total.value))
|
|
|
20
18
|
const masteryPct = computed(() => Math.round((st.value.avg_mastery || 0) * 100))
|
|
21
19
|
const todo = computed(() => (st.value.due || 0) + (st.value.fresh || 0))
|
|
22
20
|
|
|
23
|
-
/* 遗忘曲线:Ebbinghaus R = e^(-t/S)。
|
|
24
|
-
SM-2 的语义是「复习时保留率约 90%」,由 R=0.9 反推稳定度
|
|
25
|
-
S ≈ interval / ln(1/0.9) ≈ 9.49 × interval;EF 越高衰减越慢。
|
|
26
|
-
新卡没有 interval,给一个很小的 S,曲线会陡降 —— 这正是「不复习就忘」的直观样子。 */
|
|
27
21
|
const W = 620
|
|
28
22
|
const H = 130
|
|
29
23
|
const DAYS = 30
|
|
@@ -32,8 +26,6 @@ const curve = computed(() => {
|
|
|
32
26
|
const c = card.value
|
|
33
27
|
const interval = c?.interval || 0
|
|
34
28
|
const ef = c?.ease_factor || 2.5
|
|
35
|
-
/* 新卡没有 interval,S 给 1.2 让曲线在 30 天窗口里真能看出衰减;
|
|
36
|
-
取 0 会让整条曲线贴死底边,看上去像渲染坏了。 */
|
|
37
29
|
const S = interval > 0 ? 9.49 * interval * (ef / 2.5) : 1.2
|
|
38
30
|
|
|
39
31
|
const pts = []
|
|
@@ -50,13 +42,10 @@ const curve = computed(() => {
|
|
|
50
42
|
due: showDot
|
|
51
43
|
? { x: (dueDay / DAYS) * W, y: H - Math.exp(-dueDay / S) * H }
|
|
52
44
|
: null,
|
|
53
|
-
/* 今天的位置(曲线起点)保留率恒为 1 */
|
|
54
45
|
now: { x: 0, y: 0 },
|
|
55
46
|
}
|
|
56
47
|
})
|
|
57
48
|
|
|
58
|
-
/* by_day 是稀疏字典,补齐 7 天才能画成等距柱 */
|
|
59
|
-
/* 不到一天时说「几小时后」比说「0 天后」有用得多 */
|
|
60
49
|
const dueText = computed(() => {
|
|
61
50
|
const c = card.value
|
|
62
51
|
if (!c || !c.interval) return '新卡:曲线是「不复习就忘」的示意'
|
|
@@ -210,13 +199,10 @@ async function grade(g) {
|
|
|
210
199
|
gap: 22px;
|
|
211
200
|
align-items: start;
|
|
212
201
|
}
|
|
213
|
-
/* 断点看面板容器宽度,不是视口 —— 中栏只占窗口一半,
|
|
214
|
-
1600 的窗口里也只有 ~580px,按视口判断会永远两列挤成两条窄条。 */
|
|
215
202
|
@container (max-width: 820px) {
|
|
216
203
|
.rv-grid { grid-template-columns: minmax(0, 1fr); gap: 20px; }
|
|
217
204
|
}
|
|
218
205
|
|
|
219
|
-
/* ── 卡片 ── */
|
|
220
206
|
.card {
|
|
221
207
|
padding: 16px 18px 18px;
|
|
222
208
|
border-radius: var(--r-md);
|
|
@@ -269,7 +255,6 @@ async function grade(g) {
|
|
|
269
255
|
.done p, .idle p { font-size: 12px; line-height: 1.8; color: var(--dim); margin: 6px 0 14px; }
|
|
270
256
|
.idle b { color: var(--ink); }
|
|
271
257
|
|
|
272
|
-
/* ── 曲线 ── */
|
|
273
258
|
.curve {
|
|
274
259
|
display: block; width: 100%; height: 130px;
|
|
275
260
|
overflow: visible;
|
|
@@ -292,7 +277,6 @@ async function grade(g) {
|
|
|
292
277
|
color: #8a6a24;
|
|
293
278
|
}
|
|
294
279
|
|
|
295
|
-
/* ── 7 天负荷 ── */
|
|
296
280
|
.load { display: flex; align-items: flex-end; gap: 6px; padding: 8px 2px 0; }
|
|
297
281
|
.load .bar {
|
|
298
282
|
flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
|
|
@@ -5,17 +5,18 @@ import { getJSON } from '../../api.js'
|
|
|
5
5
|
import { currentLabel, loadModels, modelOptions, selectModel } from '../../stores/model.js'
|
|
6
6
|
import { setModule } from '../../stores/app.js'
|
|
7
7
|
import { setMode, chat, say } from '../../stores/chat.js'
|
|
8
|
+
import { profile, loadProfile, saveNickname } from '../../stores/profile.js'
|
|
9
|
+
import { loadSystemInfo, system } from '../../stores/system.js'
|
|
10
|
+
import { i18n, LOCALES, setLocale, t } from '../../lib/i18n.js'
|
|
8
11
|
|
|
9
|
-
/* __APP_INFO__ 由 vite define 在构建时注入 { git:{sha,ts}, built };没有 git 时是 dev。
|
|
10
|
-
用 typeof 兜一层:万一在没走过 vite 的环境里被单独加载,不要抛 ReferenceError。 */
|
|
11
12
|
const info = typeof __APP_INFO__ === 'undefined' ? null : __APP_INFO__
|
|
12
13
|
const sha = info?.git?.sha || 'dev'
|
|
13
|
-
const age = (() => {
|
|
14
|
+
const age = computed(() => {
|
|
14
15
|
const ts = info?.git?.ts || 0
|
|
15
|
-
if (!ts) return '
|
|
16
|
+
if (!ts) return t('settings.buildLocal')
|
|
16
17
|
const d = Math.floor((Date.now() - ts) / 86400000)
|
|
17
|
-
return d <= 0 ? '
|
|
18
|
-
})
|
|
18
|
+
return d <= 0 ? t('settings.buildToday') : t('settings.buildDays', { n: d })
|
|
19
|
+
})
|
|
19
20
|
|
|
20
21
|
const open = ref('') // '' | 'model' | 'keys' —— 一次只展开一组,行数不会失控
|
|
21
22
|
const up = ref(null) // null=探测中 | true | false
|
|
@@ -24,23 +25,59 @@ const err = ref('')
|
|
|
24
25
|
const cur = computed(() => currentLabel())
|
|
25
26
|
const options = computed(() => modelOptions())
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
['
|
|
31
|
-
['
|
|
32
|
-
['
|
|
33
|
-
['
|
|
34
|
-
['
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
const KEYS = computed(() => [
|
|
29
|
+
['Enter', t('key.enter')],
|
|
30
|
+
['Shift + Enter', t('key.shiftEnter')],
|
|
31
|
+
['/', t('key.slash')],
|
|
32
|
+
['@', t('key.at')],
|
|
33
|
+
['↑ ↓', t('key.arrows')],
|
|
34
|
+
['Esc', t('key.esc')],
|
|
35
|
+
['Ctrl + ,', t('key.ctrlComma')],
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const nickDraft = ref('')
|
|
38
39
|
|
|
39
40
|
onMounted(() => {
|
|
40
41
|
loadModels()
|
|
42
|
+
loadSystemInfo()
|
|
43
|
+
loadProfile().then(() => { nickDraft.value = profile.nickname })
|
|
41
44
|
probe()
|
|
42
45
|
})
|
|
43
46
|
|
|
47
|
+
async function saveNick() {
|
|
48
|
+
try {
|
|
49
|
+
await saveNickname(nickDraft.value)
|
|
50
|
+
say(profile.nickname ? t('settings.saidNick', { name: profile.nickname }) : t('settings.saidNickCleared'))
|
|
51
|
+
open.value = ''
|
|
52
|
+
} catch (e) {
|
|
53
|
+
say(t('settings.errSaveNick', { msg: e.message }), 4000)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pickLang(id) {
|
|
58
|
+
setLocale(id)
|
|
59
|
+
const name = LOCALES.find((l) => l.id === id)?.label || id
|
|
60
|
+
say(t('settings.saidLang', { name }))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const upd = computed(() => system.update || null)
|
|
64
|
+
const updLabel = computed(() => {
|
|
65
|
+
const u = upd.value
|
|
66
|
+
if (!u) return '…'
|
|
67
|
+
if (!u.enabled) return t('settings.updNotEnabled')
|
|
68
|
+
if (u.update_available) return t('settings.updAvailable', { v: u.latest })
|
|
69
|
+
if (u.latest) return t('settings.updLatest')
|
|
70
|
+
return u.error ? t('settings.updFailed') : t('settings.updPending')
|
|
71
|
+
})
|
|
72
|
+
const updHint = computed(() => {
|
|
73
|
+
const u = upd.value
|
|
74
|
+
if (!u) return t('common.loading')
|
|
75
|
+
if (!u.enabled) return t('settings.updHintOff')
|
|
76
|
+
if (u.error) return t('settings.updHintError', { msg: u.error })
|
|
77
|
+
if (u.update_available) return t('settings.updHintNew', { cur: u.current, latest: u.latest })
|
|
78
|
+
return t('settings.updHintOk', { cur: u.current })
|
|
79
|
+
})
|
|
80
|
+
|
|
44
81
|
async function probe() {
|
|
45
82
|
up.value = null
|
|
46
83
|
try {
|
|
@@ -53,6 +90,7 @@ async function probe() {
|
|
|
53
90
|
|
|
54
91
|
async function refresh() {
|
|
55
92
|
await loadModels(true).catch(() => {})
|
|
93
|
+
loadSystemInfo(true) // 更新状态也刷一次(后端 24h 缓存内不会真的外呼)
|
|
56
94
|
probe()
|
|
57
95
|
}
|
|
58
96
|
|
|
@@ -64,16 +102,16 @@ async function pick(o) {
|
|
|
64
102
|
name: o.source === 'custom' ? o.key : null,
|
|
65
103
|
})
|
|
66
104
|
open.value = ''
|
|
67
|
-
say(
|
|
105
|
+
say(t('settings.saidSwitched', { label: o.label }))
|
|
68
106
|
} catch (e) {
|
|
69
107
|
err.value = e.message
|
|
70
|
-
say(
|
|
108
|
+
say(t('settings.errSwitch', { msg: e.message }), 4000)
|
|
71
109
|
}
|
|
72
110
|
}
|
|
73
111
|
|
|
74
112
|
function mode(m) {
|
|
75
113
|
setMode(m)
|
|
76
|
-
say(m === 'stream' ? '
|
|
114
|
+
say(m === 'stream' ? t('settings.saidStream') : t('settings.saidOrchestrate'))
|
|
77
115
|
}
|
|
78
116
|
|
|
79
117
|
const ICONS = {
|
|
@@ -82,19 +120,20 @@ const ICONS = {
|
|
|
82
120
|
chat: ['M19.6 14.8a1.8 1.8 0 0 1-1.8 1.8H8.6L5 20v-3.4H6.2A1.8 1.8 0 0 1 4.4 14.8V6.4A1.8 1.8 0 0 1 6.2 4.6h11.6a1.8 1.8 0 0 1 1.8 1.8z'],
|
|
83
121
|
keys: ['M3.8 8.2h16.4v7.6H3.8z', 'M7 11h.01', 'M11 11h.01', 'M15 11h.01', 'M9 13.8h6'],
|
|
84
122
|
server: ['M4.4 5.4h15.2v4.8H4.4z', 'M4.4 13.8h15.2v4.8H4.4z', 'M7.4 7.8h.01', 'M7.4 16.2h.01'],
|
|
123
|
+
globe: ['M12 3.4a8.6 8.6 0 1 0 0 17.2 8.6 8.6 0 0 0 0-17.2z', 'M3.6 12h16.8', 'M12 3.4c2.2 2.4 3.4 5.4 3.4 8.6s-1.2 6.2-3.4 8.6c-2.2-2.4-3.4-5.4-3.4-8.6s1.2-6.2 3.4-8.6z'],
|
|
85
124
|
}
|
|
86
125
|
|
|
87
126
|
const icon = (n) => ICONS[n] || ICONS.gear
|
|
88
127
|
</script>
|
|
89
128
|
|
|
90
129
|
<template>
|
|
91
|
-
<PanelBox title="
|
|
130
|
+
<PanelBox :title="t('settings.title')" tag="Settings" state="idle" :ready="true" @refresh="refresh">
|
|
92
131
|
<div class="card">
|
|
93
|
-
<div class="grp"
|
|
132
|
+
<div class="grp">{{ t('settings.groupModel') }}</div>
|
|
94
133
|
|
|
95
134
|
<button class="row" :aria-expanded="open === 'model'" @click="open = open === 'model' ? '' : 'model'">
|
|
96
135
|
<span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('gear')" :key="i" :d="d" /></svg></span>
|
|
97
|
-
<span class="m"><b
|
|
136
|
+
<span class="m"><b>{{ t('settings.modelProvider') }}</b><span>{{ t('settings.modelProviderSub') }}</span></span>
|
|
98
137
|
<span class="hint"><i class="dot" :style="{ background: cur.color }" />{{ cur.name }}</span>
|
|
99
138
|
<span class="chev" :class="{ open: open === 'model' }">›</span>
|
|
100
139
|
</button>
|
|
@@ -109,38 +148,59 @@ const icon = (n) => ICONS[n] || ICONS.gear
|
|
|
109
148
|
>
|
|
110
149
|
<span class="dot" :style="{ background: o.color }" />
|
|
111
150
|
<span class="m"><b>{{ o.label }}</b><span>{{ o.sub }}</span></span>
|
|
112
|
-
<span v-if="o.active" class="pn-tag green"
|
|
113
|
-
<span v-else-if="o.configured === false" class="pn-tag amber"
|
|
151
|
+
<span v-if="o.active" class="pn-tag green">{{ t('settings.current') }}</span>
|
|
152
|
+
<span v-else-if="o.configured === false" class="pn-tag amber">{{ t('settings.unconfigured') }}</span>
|
|
114
153
|
</button>
|
|
115
154
|
<p class="tip">
|
|
116
|
-
|
|
155
|
+
{{ t('settings.providerNote') }}
|
|
117
156
|
</p>
|
|
118
157
|
<p v-if="err" class="tip err">{{ err }}</p>
|
|
119
158
|
</div>
|
|
120
159
|
|
|
121
160
|
<button class="row" @click="setModule('usage')">
|
|
122
161
|
<span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('gauge')" :key="i" :d="d" /></svg></span>
|
|
123
|
-
<span class="m"><b
|
|
162
|
+
<span class="m"><b>{{ t('settings.usage') }}</b><span>{{ t('settings.usageSub') }}</span></span>
|
|
124
163
|
<span class="chev">›</span>
|
|
125
164
|
</button>
|
|
126
165
|
</div>
|
|
127
166
|
|
|
128
167
|
<div class="card">
|
|
129
|
-
<div class="grp"
|
|
168
|
+
<div class="grp">{{ t('settings.groupChat') }}</div>
|
|
130
169
|
|
|
131
170
|
<div class="row static">
|
|
132
171
|
<span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('chat')" :key="i" :d="d" /></svg></span>
|
|
133
|
-
<span class="m"><b
|
|
172
|
+
<span class="m"><b>{{ t('settings.mode') }}</b><span>{{ t('settings.modeSub') }}</span></span>
|
|
134
173
|
<span class="seg">
|
|
135
|
-
<button :class="{ on: chat.mode === 'stream' }" @click="mode('stream')"
|
|
136
|
-
<button :class="{ on: chat.mode === 'orchestrate' }" @click="mode('orchestrate')"
|
|
174
|
+
<button :class="{ on: chat.mode === 'stream' }" @click="mode('stream')">{{ t('settings.stream') }}</button>
|
|
175
|
+
<button :class="{ on: chat.mode === 'orchestrate' }" @click="mode('orchestrate')">{{ t('settings.orchestrate') }}</button>
|
|
137
176
|
</span>
|
|
138
177
|
</div>
|
|
139
178
|
|
|
179
|
+
<button class="row" :aria-expanded="open === 'nick'" @click="open = open === 'nick' ? '' : 'nick'">
|
|
180
|
+
<span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('chat')" :key="i" :d="d" /></svg></span>
|
|
181
|
+
<span class="m"><b>{{ t('settings.nickname') }}</b><span>{{ t('settings.nicknameSub') }}</span></span>
|
|
182
|
+
<span class="hint">{{ profile.nickname || t('common.notSet') }}</span>
|
|
183
|
+
<span class="chev" :class="{ open: open === 'nick' }">›</span>
|
|
184
|
+
</button>
|
|
185
|
+
|
|
186
|
+
<div v-if="open === 'nick'" class="sub">
|
|
187
|
+
<div class="nick-edit">
|
|
188
|
+
<input
|
|
189
|
+
v-model="nickDraft"
|
|
190
|
+
class="nick-in"
|
|
191
|
+
maxlength="24"
|
|
192
|
+
:placeholder="t('settings.nicknamePh')"
|
|
193
|
+
@keydown.enter.prevent="saveNick"
|
|
194
|
+
/>
|
|
195
|
+
<button class="nick-save" :disabled="profile.saving" @click="saveNick">{{ t('common.save') }}</button>
|
|
196
|
+
</div>
|
|
197
|
+
<p class="tip">{{ t('settings.nicknameNote') }}</p>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
140
200
|
<button class="row" :aria-expanded="open === 'keys'" @click="open = open === 'keys' ? '' : 'keys'">
|
|
141
201
|
<span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('keys')" :key="i" :d="d" /></svg></span>
|
|
142
|
-
<span class="m"><b
|
|
143
|
-
<span class="hint">{{ KEYS.length }}
|
|
202
|
+
<span class="m"><b>{{ t('settings.shortcuts') }}</b><span>{{ t('settings.shortcutsSub') }}</span></span>
|
|
203
|
+
<span class="hint">{{ t('common.count', { n: KEYS.length }) }}</span>
|
|
144
204
|
<span class="chev" :class="{ open: open === 'keys' }">›</span>
|
|
145
205
|
</button>
|
|
146
206
|
|
|
@@ -153,20 +213,48 @@ const icon = (n) => ICONS[n] || ICONS.gear
|
|
|
153
213
|
</div>
|
|
154
214
|
|
|
155
215
|
<div class="card">
|
|
156
|
-
<div class="grp"
|
|
216
|
+
<div class="grp">{{ t('settings.groupUi') }}</div>
|
|
217
|
+
|
|
218
|
+
<div class="row static">
|
|
219
|
+
<span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('globe')" :key="i" :d="d" /></svg></span>
|
|
220
|
+
<span class="m"><b>{{ t('settings.language') }}</b><span>{{ t('settings.languageSub') }}</span></span>
|
|
221
|
+
<span class="seg">
|
|
222
|
+
<button
|
|
223
|
+
v-for="l in LOCALES"
|
|
224
|
+
:key="l.id"
|
|
225
|
+
:class="{ on: i18n.locale === l.id }"
|
|
226
|
+
@click="pickLang(l.id)"
|
|
227
|
+
>{{ l.label }}</button>
|
|
228
|
+
</span>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<p class="tip">{{ t('settings.languageNote') }}</p>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div class="card">
|
|
235
|
+
<div class="grp">{{ t('settings.groupSystem') }}</div>
|
|
157
236
|
|
|
158
|
-
<button class="row" :title="up ? '
|
|
237
|
+
<button class="row" :title="up ? t('settings.reprobe') : t('settings.retry')" @click="probe">
|
|
159
238
|
<span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('server')" :key="i" :d="d" /></svg></span>
|
|
160
|
-
<span class="m"
|
|
239
|
+
<span class="m">
|
|
240
|
+
<b>{{ t('settings.backend') }}</b>
|
|
241
|
+
<span>/api · {{ up === null ? t('settings.probing') : up ? t('common.online') : t('common.offlineShort') }}</span>
|
|
242
|
+
</span>
|
|
161
243
|
<span class="pn-tag" :class="up ? 'green' : up === false ? 'amber' : ''">
|
|
162
|
-
{{ up === null ? '…' : up ? '
|
|
244
|
+
{{ up === null ? '…' : up ? t('settings.ok') : t('settings.retry') }}
|
|
163
245
|
</span>
|
|
164
246
|
</button>
|
|
247
|
+
|
|
248
|
+
<div class="row static">
|
|
249
|
+
<span class="ic"><svg viewBox="0 0 24 24"><path v-for="(d, i) in icon('gauge')" :key="i" :d="d" /></svg></span>
|
|
250
|
+
<span class="m"><b>{{ t('settings.updates') }}</b><span>{{ updHint }}</span></span>
|
|
251
|
+
<span class="pn-tag" :class="upd?.update_available ? 'amber' : ''">{{ updLabel }}</span>
|
|
252
|
+
</div>
|
|
165
253
|
</div>
|
|
166
254
|
|
|
167
255
|
<div class="foot">
|
|
168
256
|
<span class="mono">git@{{ sha }} · {{ age }}</span>
|
|
169
|
-
<span class="dev" title="
|
|
257
|
+
<span class="dev" :title="t('settings.buildLocal')">▭</span>
|
|
170
258
|
</div>
|
|
171
259
|
</PanelBox>
|
|
172
260
|
</template>
|
|
@@ -235,6 +323,22 @@ const icon = (n) => ICONS[n] || ICONS.gear
|
|
|
235
323
|
.tip { padding: 4px 8px 2px; font-size: 11.5px; line-height: 1.6; color: var(--dim); }
|
|
236
324
|
.tip.err { color: var(--red); }
|
|
237
325
|
|
|
326
|
+
.nick-edit { display: flex; align-items: center; gap: 6px; padding: 4px 8px 2px; }
|
|
327
|
+
.nick-in {
|
|
328
|
+
flex: 1; min-width: 0;
|
|
329
|
+
padding: 5px 8px; border-radius: 7px;
|
|
330
|
+
font-size: 12.5px; color: var(--ink);
|
|
331
|
+
background: #fff; border: 1px solid var(--line);
|
|
332
|
+
}
|
|
333
|
+
.nick-in:focus { outline: none; border-color: rgba(26, 157, 95, 0.5); }
|
|
334
|
+
.nick-save {
|
|
335
|
+
flex-shrink: 0;
|
|
336
|
+
padding: 5px 11px; border-radius: 7px;
|
|
337
|
+
font-size: 12px; font-weight: 600; color: #fff;
|
|
338
|
+
background: var(--green);
|
|
339
|
+
}
|
|
340
|
+
.nick-save:disabled { opacity: 0.45; }
|
|
341
|
+
|
|
238
342
|
.kv { display: flex; align-items: center; gap: 10px; padding: 4px 8px; }
|
|
239
343
|
.kv kbd {
|
|
240
344
|
min-width: 92px; flex-shrink: 0;
|
|
@@ -10,7 +10,6 @@ onMounted(() => loadPanel('tasks'))
|
|
|
10
10
|
const ready = computed(() => p.state === 'ok' && Object.keys(p.sched).length > 0)
|
|
11
11
|
const byState = computed(() => p.procStats?.by_state || {})
|
|
12
12
|
|
|
13
|
-
/* 后端把每个任务建模成「进程」,状态机 created→queued→scheduled→running→completed/failed */
|
|
14
13
|
function stateTag(s) {
|
|
15
14
|
return s === 'completed' ? 'green' : s === 'failed' ? 'red' : s === 'running' ? 'amber' : ''
|
|
16
15
|
}
|
|
@@ -44,9 +44,6 @@ async function flip(s) {
|
|
|
44
44
|
busy.value = ''
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/* ── 单技能测试:POST /ahs/skills/{id}/test ──
|
|
48
|
-
走真管线(权限/断路器/SSRF),所以界面要能填参数,也要把 needs_confirmation /
|
|
49
|
-
blocked / circuit_open 这些非成功态原样显示,不能只画个绿勾。 */
|
|
50
47
|
const tId = ref('')
|
|
51
48
|
const tArgs = ref({})
|
|
52
49
|
const tBusy = ref(false)
|
|
@@ -74,7 +71,6 @@ function beginTest(s) {
|
|
|
74
71
|
err.value = ''
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
/* params 是 {名: 类型} 的朴素 schema,类型只是提示;这里按声明做最小转换,其余原样送字符串 */
|
|
78
74
|
function coerce(params, args) {
|
|
79
75
|
const out = {}
|
|
80
76
|
for (const [k, ty] of Object.entries(params || {})) {
|
|
@@ -133,7 +129,6 @@ async function dropSkill(s) {
|
|
|
133
129
|
|
|
134
130
|
const splitList = (t) => String(t || '').split(/[,,\s]+/).map((s) => s.trim()).filter(Boolean)
|
|
135
131
|
|
|
136
|
-
/* "city:str, days:int" → {city:"str", days:"int"} */
|
|
137
132
|
function parseParams(txt) {
|
|
138
133
|
const out = {}
|
|
139
134
|
for (const part of String(txt || '').split(/[,,;\n]/)) {
|
|
@@ -145,7 +140,6 @@ function parseParams(txt) {
|
|
|
145
140
|
return out
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
/* "Authorization: Bearer x" 每行一条 → dict(空行 / 无冒号行忽略) */
|
|
149
143
|
function parseHeaders(txt) {
|
|
150
144
|
const out = {}
|
|
151
145
|
for (const line of String(txt || '').split('\n')) {
|
|
@@ -155,8 +149,6 @@ function parseHeaders(txt) {
|
|
|
155
149
|
return out
|
|
156
150
|
}
|
|
157
151
|
|
|
158
|
-
/* ── 安装技能:POST /ahs/skills ──
|
|
159
|
-
后端只收 web_api / mcp;审核门 deny → 403, warn → 409 needs_ack(要再点一次「已知风险」) */
|
|
160
152
|
const SIDE = { read: 'read · 只读', write: 'write · 写入', send: 'send · 代发', pay: 'pay · 付款' }
|
|
161
153
|
const showIns = ref(false)
|
|
162
154
|
const insBusy = ref(false)
|
|
@@ -220,8 +212,93 @@ async function doInstall(ack = false) {
|
|
|
220
212
|
}
|
|
221
213
|
}
|
|
222
214
|
|
|
223
|
-
|
|
224
|
-
|
|
215
|
+
const MI_MAX = 4 * 1024 * 1024
|
|
216
|
+
const miOpen = ref(false)
|
|
217
|
+
const miBusy = ref(false)
|
|
218
|
+
const miErr = ref('')
|
|
219
|
+
const miOk = ref('')
|
|
220
|
+
const miAck = ref(false)
|
|
221
|
+
const miText = ref('')
|
|
222
|
+
const miFile = ref('')
|
|
223
|
+
const miB64 = ref('') // zip 的 base64;非空时优先于 miText
|
|
224
|
+
const miRef = ref(null)
|
|
225
|
+
|
|
226
|
+
function toggleMi() {
|
|
227
|
+
miOpen.value = !miOpen.value
|
|
228
|
+
miErr.value = ''
|
|
229
|
+
miOk.value = ''
|
|
230
|
+
miAck.value = false
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resetMi() {
|
|
234
|
+
miText.value = ''
|
|
235
|
+
miFile.value = ''
|
|
236
|
+
miB64.value = ''
|
|
237
|
+
miAck.value = false
|
|
238
|
+
if (miRef.value) miRef.value.value = ''
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function toB64(file) {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
const fr = new FileReader()
|
|
244
|
+
fr.onload = () => {
|
|
245
|
+
const s = String(fr.result || '')
|
|
246
|
+
resolve(s.slice(s.indexOf(',') + 1)) // 去掉 data:...;base64, 前缀
|
|
247
|
+
}
|
|
248
|
+
fr.onerror = () => reject(new Error('读取文件失败'))
|
|
249
|
+
fr.readAsDataURL(file)
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function onMiFile(ev) {
|
|
254
|
+
const f = ev.target.files && ev.target.files[0]
|
|
255
|
+
miFile.value = ''
|
|
256
|
+
miB64.value = ''
|
|
257
|
+
miErr.value = ''
|
|
258
|
+
miOk.value = ''
|
|
259
|
+
miAck.value = false
|
|
260
|
+
if (!f) return
|
|
261
|
+
miFile.value = `${f.name} · ${(f.size / 1024).toFixed(0)} KB`
|
|
262
|
+
if (f.size > MI_MAX) { miErr.value = '文件超过 4MB,拒绝上传'; return }
|
|
263
|
+
try {
|
|
264
|
+
if (/\.zip$/i.test(f.name)) {
|
|
265
|
+
miB64.value = await toB64(f)
|
|
266
|
+
miText.value = ''
|
|
267
|
+
} else {
|
|
268
|
+
miText.value = await f.text()
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
miErr.value = String(e.message || e)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function doMdInstall(ack = false) {
|
|
276
|
+
if (miBusy.value) return
|
|
277
|
+
const content = miText.value.trim()
|
|
278
|
+
if (!ack && !content && !miB64.value) {
|
|
279
|
+
miErr.value = '粘贴 SKILL.md 正文,或选一个 .md / .zip 文件'
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
miBusy.value = true
|
|
283
|
+
miErr.value = ''
|
|
284
|
+
miOk.value = ''
|
|
285
|
+
const body = { acknowledged: ack }
|
|
286
|
+
if (miB64.value) body.package_b64 = miB64.value
|
|
287
|
+
else body.content = content
|
|
288
|
+
try {
|
|
289
|
+
const d = await postJSON('/ahs/md-skills', body)
|
|
290
|
+
const name = (d.skill && (d.skill.name || d.skill.id)) || '指令技能'
|
|
291
|
+
miOk.value = `${d.created ? '已安装' : '已更新'}「${name}」`
|
|
292
|
+
resetMi()
|
|
293
|
+
await refreshPanel('tools')
|
|
294
|
+
} catch (e) {
|
|
295
|
+
miErr.value = String(e.message || e)
|
|
296
|
+
miAck.value = !!(e.detail && e.detail.needs_ack)
|
|
297
|
+
} finally {
|
|
298
|
+
miBusy.value = false
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
225
302
|
const showMcpAdd = ref(false)
|
|
226
303
|
const mcpBusy = ref(false)
|
|
227
304
|
const mcpErr = ref('')
|
|
@@ -265,7 +342,6 @@ async function addMcp(ack = false) {
|
|
|
265
342
|
}
|
|
266
343
|
}
|
|
267
344
|
|
|
268
|
-
/* 重连:server 那边的工具会变,POST .../refresh 增量对齐注册表 */
|
|
269
345
|
async function refreshMcp(s) {
|
|
270
346
|
if (mcpBusy.value) return
|
|
271
347
|
mcpBusy.value = true
|
|
@@ -392,6 +468,43 @@ async function delMcp(s) {
|
|
|
392
468
|
</div>
|
|
393
469
|
</div>
|
|
394
470
|
|
|
471
|
+
<div class="pn-sec">
|
|
472
|
+
<div class="pn-h">
|
|
473
|
+
安装指令技能
|
|
474
|
+
<button class="mbtn" @click="toggleMi">{{ miOpen ? '收起' : '+ 上传' }}</button>
|
|
475
|
+
</div>
|
|
476
|
+
<div v-if="miOpen" class="tk-form">
|
|
477
|
+
<label class="f"><span>SKILL.md 或 OpenClaw 包(.md / .zip)</span>
|
|
478
|
+
<input ref="miRef" type="file" accept=".md,.markdown,.zip" @change="onMiFile" />
|
|
479
|
+
</label>
|
|
480
|
+
<div v-if="miFile" class="pn-note">{{ miFile }}</div>
|
|
481
|
+
<label class="f"><span>或粘贴正文(frontmatter + 指示)</span>
|
|
482
|
+
<textarea
|
|
483
|
+
v-model="miText"
|
|
484
|
+
rows="8"
|
|
485
|
+
spellcheck="false"
|
|
486
|
+
:disabled="!!miB64"
|
|
487
|
+
placeholder="--- name: my-skill / description: 什么时候用它 --- 然后写正文"
|
|
488
|
+
/>
|
|
489
|
+
</label>
|
|
490
|
+
<div class="fbar">
|
|
491
|
+
<button class="go" :disabled="miBusy" @click="doMdInstall(false)">
|
|
492
|
+
{{ miBusy ? '安装中…' : '安装' }}
|
|
493
|
+
</button>
|
|
494
|
+
<button v-if="miAck" class="go warn" :disabled="miBusy" @click="doMdInstall(true)">
|
|
495
|
+
已知风险,继续安装
|
|
496
|
+
</button>
|
|
497
|
+
<span v-if="miErr" class="tip bad">{{ miErr }}</span>
|
|
498
|
+
<span v-else-if="miOk" class="tip ok">{{ miOk }}</span>
|
|
499
|
+
<span v-else class="tip dim">只取 SKILL.md 与 _meta.json;包内 scripts/ 不落地、不执行</span>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
<div v-else class="pn-note">
|
|
503
|
+
指令技能是注入提示词的 markdown,不是可执行工具。命令行也能装:
|
|
504
|
+
<code>foxclaw skills install @owner/slug</code>、<code>github:owner/repo</code>、<code>./pkg.zip</code>。
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
395
508
|
<div class="pn-sec">
|
|
396
509
|
<div class="pn-h">
|
|
397
510
|
安装技能
|
|
@@ -532,11 +645,6 @@ async function delMcp(s) {
|
|
|
532
645
|
<button class="mbtn danger" @click="delMcp(s)">移除</button>
|
|
533
646
|
</div>
|
|
534
647
|
</div>
|
|
535
|
-
|
|
536
|
-
<div class="pn-note">
|
|
537
|
-
指令技能正文 <code>/api/ahs/md-skills/{id}</code>,执行技能运行 <code>POST /api/ahs/skills/{id}/test</code>;
|
|
538
|
-
连接器与技能面板是同一份数据(<code>/api/ahs/mcp/servers</code>)。
|
|
539
|
-
</div>
|
|
540
648
|
</PanelBox>
|
|
541
649
|
</template>
|
|
542
650
|
|
|
@@ -550,7 +658,6 @@ async function delMcp(s) {
|
|
|
550
658
|
.sw.on { background: rgba(26, 157, 95, 0.12); color: var(--green); }
|
|
551
659
|
.sw:disabled { opacity: 0.5; }
|
|
552
660
|
|
|
553
|
-
/* 行内小按钮:测试 / 删除 / 新增 —— 原来「执行技能」整行是死的,点了没反应 */
|
|
554
661
|
.mbtn {
|
|
555
662
|
flex-shrink: 0; padding: 4px 10px; border-radius: var(--r-pill);
|
|
556
663
|
font-size: 11px; font-family: var(--mono); color: var(--dim);
|
|
@@ -562,7 +669,6 @@ async function delMcp(s) {
|
|
|
562
669
|
.mbtn.danger:hover { color: var(--red); border-color: rgba(229, 72, 77, 0.35); }
|
|
563
670
|
.mbtn:disabled { opacity: 0.5; }
|
|
564
671
|
|
|
565
|
-
/* ── 表单(安装技能 / 接入连接器 / 技能测试)── */
|
|
566
672
|
.tk-form, .tk-test {
|
|
567
673
|
margin: 4px 0 6px;
|
|
568
674
|
padding: 11px 12px;
|
|
@@ -595,6 +701,7 @@ async function delMcp(s) {
|
|
|
595
701
|
.f textarea { resize: vertical; line-height: 1.6; }
|
|
596
702
|
.f input:focus, .f select:focus, .f textarea:focus { border-color: var(--line); background: #fff; }
|
|
597
703
|
.f input::placeholder, .f textarea::placeholder { color: var(--dim); }
|
|
704
|
+
.f input[type='file'] { padding: 4px 6px; font-size: 11.5px; color: var(--dim); }
|
|
598
705
|
|
|
599
706
|
.chk {
|
|
600
707
|
display: flex; align-items: center; gap: 6px;
|
|
@@ -622,7 +729,6 @@ async function delMcp(s) {
|
|
|
622
729
|
.tip.ok { color: var(--green); }
|
|
623
730
|
.tip.bad { color: var(--red); white-space: normal; }
|
|
624
731
|
|
|
625
|
-
/* 测试结果:成功/失败都原文展示,失败(needs_confirmation/blocked)不涂成成功色 */
|
|
626
732
|
.tk-res {
|
|
627
733
|
margin-top: 4px; padding: 8px 10px; border-radius: var(--r-sm);
|
|
628
734
|
background: rgba(23, 32, 26, 0.03); border: 1px solid var(--line-soft);
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
/* 7 个面板共用的时间/数字格式化。集中一份,避免每个组件各写一遍还写得不一样。 */
|
|
2
1
|
|
|
3
|
-
/* 后端时间戳三种形态都有:秒 float(事件总线)、毫秒 int、ISO 字符串(记忆 created_at)。
|
|
4
|
-
字符串交给 Date 自己解析,数字按量级判断。 */
|
|
5
2
|
function toDate(ts) {
|
|
6
3
|
if (!ts) return null
|
|
7
4
|
if (typeof ts === 'string') {
|
|
@@ -41,7 +38,6 @@ export function fmtMs(ms) {
|
|
|
41
38
|
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
/* 截断到 n 个字符(中文按 1 计),用于行内摘要 */
|
|
45
41
|
export function clip(s, n = 46) {
|
|
46
42
|
const t = String(s ?? '').replace(/\s+/g, ' ').trim()
|
|
47
43
|
return t.length > n ? t.slice(0, n) + '…' : t
|