feat(webui): 侧边栏布局、版本信息与快捷键优化#131
Conversation
@pytest.mark.asyncio
async def test_create_skill_writes_to_plugins_path(tmp_path, monkeypatch):
"""POST /skills should write to ~/.flocks/plugins/skills/, not ~/.flocks/skills/."""
from httpx import AsyncClient, ASGITransport
from flocks.server.app import app
# Redirect home directory to tmp_path so we don't pollute real ~/.flocks
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setattr(os.path, "expanduser", lambda p: str(tmp_path) if p == "~" else p.replace("~", str(tmp_path)))
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
try:
# Use find command for file search
cwd = os.getcwd()
# Build find command
cmd = ["find", ".", "-name", f"*{query}*"]
if type == "file":
cmd.extend(["-type", "f"])
elif type == "directory":
cmd.extend(["-type", "d"])
# Exclude common directories
excludes = [".git", "node_modules", "__pycache__", ".venv", "venv"]
for exclude in excludes:
cmd.extend(["-not", "-path", f"*/{exclude}/*"])
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
timeout=5,
)Additional Concerns
|
AgentFlocks#144) * fix(provider/google): robust Gemini 3 support with ReAct parsing, reasoning persistence, and token tracking * fix(provider/google): address PR AgentFlocks#144 review – preserve mixed chunks, forward thinking config, activate session-id DB path Three issues raised in the PR AgentFlocks#144 review are addressed end-to-end: 1. (High) chat_stream() previously emitted reasoning together with delta / tool_calls in the same StreamChunk, while SessionRunner's consumer loop treated any reasoning-bearing chunk as reasoning-only and `continue`d, silently dropping text and tool calls. The Google provider now emits reasoning, text+tool_calls, and usage as separate chunks; the runner loop is rewritten to process all three pieces of a chunk sequentially without early `continue`, so future mixed chunks from any provider also survive. 2. (High) chat() and chat_stream() no longer hard-code max_output_tokens= 8192. A new _build_generate_config() helper honours caller-provided max_tokens and thinkingConfig (built per-model by build_provider_options for Gemini 2.5 thinkingBudget / Gemini 3 thinkingLevel) and only falls back to 8192 when no value is provided. 3. (Medium) The DB-backed reasoning replay branch in _convert_messages was unreachable because ChatMessage carries no sessionID. SessionRunner now forwards session_id=self.session.id via kwargs (verified safe across all 27 providers, which all accept **kwargs and selectively extract keys). _convert_messages accepts an explicit session_id argument; legacy per-message attributes are still honoured for forward-compat. Self-review hardening: 4. _convert_messages now uses a two-phase commit so a mid-loop exception, a DB snapshot containing only system rows, or an empty cache cannot leave system_msg / raw_gemini_messages partially populated and then let the fallback path append the in-memory messages on top. Without this guard the bug-fix in (3) would itself produce a duplicated system prompt and a duplicated conversation history. Tests added (18 cases, all green): - tests/provider/test_google_gemini_fixes.py – chunk separation, thinkingConfig / max_tokens forwarding, session_id propagation, and DB-cache state-pollution edge cases. - tests/session/test_runner_chunk_handling.py – consumer loop preserves text and tool_calls in reasoning-bearing chunks, end-to-end transitions between reasoning/text/tool blocks, and usage-only chunks.
…stream-channel feat/dingtalk stream channel
…ntFlocks#173) * feat(windows): bundled browser override and npm PATH for updates - Resolve chrome.exe under bundled Chrome in bootstrap-windows.ps1 and set FLOCKS_BROWSER_EXECUTABLE_OVERRIDE - Honor explicit browser override in install.ps1 when configuring agent-browser - Prepend bundled Node to PATH for frontend npm install/build in updater alongside npm registry * fix(windows): recover from broken uv managed Python cache Retry installer dependency setup after clearing a corrupted uv-managed Python runtime so the EXE install flow can recover from stale cached interpreters. * fix(windows): harden EXE updates and PowerShell script encoding Retry Windows EXE updates after releasing locked frontend files, repair broken uv-managed Python caches during uv sync, and restore scripts/install.ps1 to UTF-8 with BOM plus CRLF so the PowerShell rules and runtime expectations stay aligned.
* feat(auth): merge feat/local_account — single-admin account system - Add login/setup-admin/force-change-password pages and auth-guarded routing - Rewrite AdminUsers page with self-service password reset - Integrate require_user guard into session routes * added english * fix(auth): unblock channel webhooks and add orphan-session backfill (AgentFlocks#175 follow-up) - Make /api/channel/{channel_id}/webhook public via PUBLIC_PATH_REGEXES so external IM platforms (DingTalk / WeCom / Feishu) can deliver callbacks without 401; plugin handle_webhook retains full signature validation ownership and no synthetic admin user is injected. - Add AuthService.reassign_orphan_sessions and `flocks admin reassign-orphan-sessions` CLI to backfill empty owner_user_id on sessions created by CLI / background / inbound dispatchers, so later-added member accounts can still see them. - Document remote-TUI / SDK API-token flow, reverse-proxy X-Forwarded-For requirement, and one-time-password operational caveats in README.md / README_zh.md (new section 4.4). - Add unit coverage for the webhook regex, the channel-webhook no-credentials passthrough, and the reassign helper (dry-run, role guard, orphan filter). * refactor(auth): self-review polish on cf72701 (AgentFlocks#175 follow-up) Driven by a deeper second pass over the previous fix: - AuthService.reassign_orphan_sessions now wraps each Session.update in its own try/except, returning a new ``failed`` counter so a single storage error no longer aborts the whole pass. - ``flocks admin reassign-orphan-sessions`` drops the duplicated admin pre-check (single-sourced in the service layer via ValueError), removes the misleading ``# noqa: UP006`` annotation and the ``{"role": -1}`` magic sentinel, surfaces ``failed`` in the CLI output, and exits with code 2 when any update failed so CI / scripts can react. - PUBLIC_PATH_REGEXES uses a non-capturing ``(?:api/)?`` group and documents (a) the built-in DingTalk / WeCom plugins still run in stream mode (so the webhook entrypoint is for external / custom plugins only) and (b) a TODO to add the provider OAuth-callback path once it leaves stub state. - README.md / README_zh.md §4.4 distinguishes server-side ``generate-api-token`` (persists on the server) from client-side ``set-api-token`` (writes the same token into the client's local secret store), adds a curl smoke test, calls out the built-in webhook caveat, and documents the new ``failed`` counter / exit-code-2 behaviour. - Add a partial-failure unit test (one orphan raises mid-loop, the other two still get rewritten) and update the existing dry-run / rewrite assertions for the new ``failed`` field.
…ks#180) * fix(cli): preserve remote backend routing for webui access Keep concrete remote backend hosts reachable from the WebUI while preserving localhost proxy behavior, and merge runtime CORS origins with configured allowlists to avoid regressions. * fix(cli,server): bracket IPv6 in URLs and simplify CORS runtime origins - Remove _FLOCKS_SERVER_* from backend subprocess env; rely on WebUI origin for CORS - Add IPv6 literal bracketing when composing backend base URL and CORS origins - Narrow windows-packaging workflow path triggers - Extend tests for IPv6 and updated CORS/service_manager behavior
…cn-llm The `threatbook-cn-llm` gateway exposes MiniMax models over an OpenAI-compatible endpoint but its streaming chunks omit the `tool_calls` field entirely (observed 2026-04 with `minimax-m2.7`: first `ChoiceDelta` only carries `content` / `role`, `model_extra_keys=[]`). With native function-calling the model can never emit a tool call, so every turn ends with `finish_reason=stop` and `tool_calls=0` — users in IM channels just see a short text reply (e.g. a couple of IPs) and no tool execution. Add `threatbook-cn-llm` to the MiniMax text-call provider whitelist so `_should_use_text_tool_call_mode()` returns True for this provider+model pair. The runner then injects the `<minimax:tool_call>` XML instructions and the existing text parser picks the calls up from the content stream, restoring tool execution end-to-end. - Other models routed through the same gateway (qwen, GLM, etc.) remain on the standard OpenAI native function-calling path. - Comment in `_should_use_text_tool_call_mode()` records the root cause for future maintainers. Tests: - New regression cases in `TestMiniMaxTextToolMode`: - threatbook-cn-llm + minimax-m2.7 → XML mode enabled - case-insensitive provider/model id handling - threatbook-cn-llm + qwen3.6-plus → XML mode disabled - `tests/session/test_runner_step.py` (48 cases) all pass. Made-with: Cursor
…tool-call-threatbook-cn-llm fix(session/runner): force MiniMax XML tool-call mode for threatbook-…
…KILL.md on demand The `skill` tool exposes two surfaces, both of which were silently dropping critical content: 1. The tool's meta-description (shipped in every system prompt) listed each skill's full frontmatter description (up to 1024 chars). Long descriptions bloated the prompt and the model often acted on a partial preview without loading the full file. 2. When the model called `skill(name=...)`, ToolRegistry's auto-truncate path silently cropped the returned SKILL.md at 10 KB / 200 lines (head-only), dropping the workflow steps, references, and constraints that authors typically place at the end of the file. Users reported "skill.md 后面的 感觉就完全丢失了". This change implements progressive disclosure (mirroring hermes-agent's `skills_list` / `skill_view` split): - `build_description()` now caps each skill's preview at MAX_SKILL_DESCRIPTION_PREVIEW_CHARS (500) using head + tail truncation, preserving both the opening (scope/triggers) and the closing (hard constraints like "必须先加载本 skill"). Adds an explicit instruction telling the model the previews are summaries and it MUST call `skill(name=...)` to load the full SKILL.md before acting. - `skill_tool_impl()` sets `truncated=True` on its `ToolResult` to opt out of the registry's auto-truncate pass, so the full SKILL.md (verified end- to-end with 18.8 KB / 500 lines `agent-browser`) reaches the model intact. Also emits a `skill.load.full_content` log event for auditability. Empirical impact on the 12 existing skills: - System prompt index: 4.6 KB total (only `tool-builder` 614→500 is trimmed) - Load on demand: previously 8/12 skills lost their tail; now all 12 are delivered in full Tests: 15 new in `tests/tool/test_skill_tool_description.py` covering truncation algorithm, build_description output, and end-to-end no-truncation through `ToolRegistry.execute()`. Made-with: Cursor
… content (AgentFlocks#184) Keep tool and service lists compact so long descriptions do not introduce nested scrollbars. Allow long descriptions and parameter docs to wrap correctly in detail drawers where users expect full content.
…pgrade-journal fix(cli): daemon backend logs, serve timestamps, upgrade journal
…tion-progressive-load fix(tool/skill): cap description preview at 500 chars and load full S…
…om providers
Self-hosted LLM gateways (vLLM, internal proxies, etc.) often expose an
OpenAI-compatible endpoint without enforcing auth, so requiring an API
key locked these users out of `openai-compatible` and `custom-*`
providers. Mandatory keys remain enforced for SaaS providers (OpenAI,
Anthropic, Google, Azure, DeepSeek, Volcengine, Alibaba, Tencent,
SiliconFlow) — behavior unchanged for them.
Backend
- routes/provider.py: `set_provider_credentials` accepts an empty
`api_key` for `openai-compatible` / `custom-*` and persists a
`not-needed` sentinel so the OpenAI SDK still constructs cleanly.
Other provider IDs continue to return HTTP 400.
- sdk/openai_base.py: new opt-in `ALLOW_NO_API_KEY` class flag; when
True, `_get_client()` falls back to `NO_API_KEY_PLACEHOLDER` instead
of raising. Default False keeps SaaS subclasses strict.
- provider.py: `DynamicOpenAIProvider` opts into `ALLOW_NO_API_KEY`,
defaults `_api_key` to the sentinel when unresolved, and overrides
`is_configured()` to treat a configured base URL alone as sufficient.
Same `_api_key` fallback for `DynamicCherryProvider` (it inherits
`is_configured` via OpenAICompatibleProvider).
- sdk/openai_compatible.py: override `is_configured()` to require an
explicit `configure()` call AND accept either api_key or base_url —
guards against the constructor's env-default seeds being misread as
"ready" before the user configures anything.
Frontend
- pages/Model/index.tsx: extract `providerAllowsEmptyApiKey()` helper
and apply it to all four API-key validation sites in the wizard and
configure dialog. Drop the `*` mark, swap placeholder, and add a
hint line for these providers.
- locales/{en-US,zh-CN}/model.json: add `apiKeyOptional`,
`apiKeyOptionalHint`, `apiKeyOptionalPlaceholder`.
Tests
- tests/server/routes/test_provider_optional_api_key.py: 13 cases
covering route acceptance, route rejection for strict providers,
dynamic provider client construction, `is_configured()` semantics
(including the regression where constructor env defaults must not
imply "configured"), and Cherry inheriting the override.
Made-with: Cursor
…ponse
GET /providers/{id}/credentials was returning the internal "not-needed"
placeholder verbatim, causing the WebUI to pre-fill the API Key input
with that literal string when users reopened the configure dialog for
openai-compatible / custom-* providers without a real API key.
Mask the sentinel back to None on read while keeping has_credential=True
so the UI knows a credential record exists. Also extract the value into
a module-level _NO_API_KEY_PLACEHOLDER constant + _is_placeholder_api_key
helper so the sentinel is defined in exactly one place and defended both
when the value comes from the secret manager and from an inline
flocks.json apiKey entry.
Adds three regression tests covering placeholder masking, real key
pass-through, and the inline-flocks.json edge case.
Made-with: Cursor
…PI key sentinel
Three small follow-ups suggested in the PR review (no behavior regression):
1. Document the (extremely unlikely) collision risk where an upstream
service might mint a real key whose literal value equals the
"not-needed" sentinel, so future maintainers know the sentinel is
reserved and where to change it.
2. Replace the naive 4/4 mask of the sentinel ("not-***eded", which can
look like a real short API key in log searches) with an explicit
"<no-auth>" marker in the credentials.saving audit log. New regression
test asserts both presence of the marker and absence of the naive
mask form.
3. Tighten the WebUI ProviderCredentials TypeScript type so optional
string fields are typed as ``string | null | undefined`` (matching
what FastAPI/Pydantic actually emits as JSON ``null``) and document
that ``api_key`` may be null even when ``has_credential`` is true,
directing UI code to ``has_credential`` for the "is configured" check.
Made-with: Cursor
…dentials After widening ProviderCredentials.secret_id / api_key_masked to ``string | null | undefined`` (so Pydantic ``null`` values type-check correctly), the providerCredentialsAdapter no longer satisfies the local Credentials interface in CredentialsCard which had ``string | undefined``. Widen the local Credentials interface symmetrically. MCPCredentials with the narrower ``string | undefined`` remains assignable as a subtype, so the mcpCredentialsAdapter path continues to compile. Made-with: Cursor
…-key-for-openai-compatible feat/optional api key for OpenAI compatible
Preserve complex object and array schemas for YAML tools so models receive the intended structure. Coerce JSON string payloads back into dict/list values while keeping plain-string compatibility for existing tools.
Made-with: Cursor
…n-v2026.4.24 chore: bump version to v2026.4.24
* fix(auth): accept Referer-only browser requests behind proxies Docker and reverse-proxy deployments may strip sec-fetch metadata while still forwarding Referer, causing browser session requests to be misclassified as remote non-browser traffic and bounce users back to login. Treat Referer as a browser signal and cover the regression with an auth compatibility test.
Mirror the existing onesec plugin pattern under
`.flocks/plugins/tools/api/onesig/`, exposing the full OneSIG Web API
(~340 endpoints, 39 doc modules) as 6 grouped tools:
onesig_login / onesig_monitoring / onesig_strategy / onesig_assets /
onesig_device / onesig_helper.
Authentication & session
- Cookie-based session via aiohttp ClientSession + CookieJar
- Login flow: GET /v3/captcha -> GET /v3/pubkey ->
RSA-OAEP(password) -> POST /v3/login -> optional POST /v3/login/totp
- JSEncrypt-compatible RSA-OAEP (default sha1, switchable to sha256
via api_services.onesig_api.oaep_hash)
- Auto re-login on HTTP 401/403 and responseCode 1019..1022
- Inline TOTP (`enableTotp=true` from /v3/captcha) and QR-scan TOTP
(responseCode 1012) both surface clear errors when missing
- 1010 / 1011 (default / expired password) routed to a hint that
points at onesig_login(action='change_password', ...)
Sensitive write operations (RSA-OAEP encrypted body fields)
ActionSpec.encrypt_fields auto-fetches a fresh /v3/pubkey and replaces
plaintext fields before sending. Applies to:
- DELETE /v3/aclog -> aclog_delete (password)
- DELETE /v3/user -> user_delete (password)
- PUT /v3/user/secret/reset -> user_secret_reset (password)
- POST /v3/user -> user_create (password,
dupPassword)
- PUT /v3/interface -> interface_update (password)
- POST /v3/device/upgrade -> device_upgrade (password)
- POST /v3/device/upgrade -> device_upgrade_upload (password,
multipart)
- PUT /v3/user/password -> change_password (each of
oldPassword / newPassword / dupPassword encrypted independently)
Multipart uploads
ActionSpec.multipart streams a local file via aiohttp.FormData under the
documented field name (default `file`, certificates use `certFile`).
Callers pass `file_path: <local absolute path>`. Wired up to:
- asset_import (assets/.csv)
- tls_cert_create / tls_cert_update (certFile field)
- basic_information_import (offline intel package)
- basic_license_upload (license file)
- system_upgrade (system upgrade package)
- device_upgrade_upload (firmware + RSA password combined)
- backup_import (config backup archive)
Binary downloads
31 export / template / pcap / coredump endpoints flagged binary=True;
the dispatcher saves the response stream to outputs/<date>/ and surfaces
saved_path / size / content_type in both output and metadata.
Observability
ToolResult.metadata now includes source / api / method / path /
http_status / response_code / verbose_msg (and saved_path for binary
endpoints) so callers can debug "HTTP 200 but responseCode != 0" and
locate downloaded files without parsing output.
Cross-validation: 340 ActionSpecs across 6 groups vs YAML enums all
align (assets=15 / device=148 / helper=6 / login=10 / monitoring=75 /
strategy=89). Reuses transitively-installed cryptography (46.x); no new
dependency required.
Made-with: Cursor
Wire the OneSEC, NGTIP, and Qingteng API handlers to respect the configured SSL verification toggle so private deployments can bypass certificate checks when needed. Add regression tests that cover both enabled and disabled verification paths and isolate Qingteng tests from local machine config.
Introduce a unified entry skill for all `onesig_*` tools, mirroring the style of `onesec-use` / `qingteng-use`. Any task that mentions OneSIG / SIG / Secure Internet Gateway must load this skill first instead of calling the tools directly. - SKILL.md: API-vs-browser decision flow and write-action confirmation protocol; declares the skill as the single decision entry-point. - references/api-reference.md: action routing table for the six grouped tools (`onesig_login` / `assets` / `device` / `helper` / `monitoring` / `strategy`), business primary keys (uniqueId / uid / pid / groupId / ruleId / srcIp / server+port, etc.), high-frequency call examples, binary/file endpoints (`document_preview`, multipart uploads), the RSA-OAEP auto-encrypted fields (`password`, `dupPassword`), the mandatory `type=physical` flag for API Key endpoints, and the fact that `ips_rule_create` is actually a query. - references/browser-workflow.md: console navigation map and `agent-browser` operating rules for the fallback path when the API is unavailable. The reference has been cross-checked against the vendor SIGWEBAPI docs and `onesig.handler.py`, fixing legacy drifts such as assetType -> type, name -> username, fileName -> id, port_protect_group_port_list -> port_protect_port_list, and the top-level placement of `condition` / `comments` in `whitelist_add`. Made-with: Cursor
…pi_prefix Three interlocking changes that make OneSIG v2.5.x easier to bring online out of the box and align SSL / cookie behaviour with the other built-in providers (onesec / ngtip / qingteng). - Persistent login session: after a successful login the aiohttp CookieJar is serialised into `~/.flocks/config/.secret.json` under a key shaped like `onesig_session_cookie__<sha1[:12]>`. On process restart, if at least one cookie is still alive the jar is rehydrated in-place and the captcha -> pubkey -> /v3/login -> /v3/account chain is skipped entirely; the very first business request that returns 401 / responseCode 1019..1022 still triggers exactly one auto re-login to preserve previous behaviour. `logout()` wipes the on-disk entry, `close()` does not. The `persist_cookies` toggle defaults to True and honours camelCase / `custom_settings` / `ONESIG_PERSIST_COOKIES` fallbacks. - SSL verification defaults to OFF, matching onesec / ngtip / qingteng. Five sources are recognised in priority order: `verify_ssl`, `ssl_verify`, `verifySsl`, `custom_settings.verify_ssl`, and the `ONESIG_VERIFY_SSL` env var, falling back to False. The `verify_ssl` field is removed from `provider.credential_fields` so the global "SSL verify" switch on `ServiceDetailPanel` becomes the single source of truth. - `api_prefix` default changes from `"/api"` to `""`, matching the common v2.5.x deployments where nginx already routes `/v3/...` to the backend. Reverse-proxy deployments can set it back to `"/api"`. `_provider.yaml` notes are updated with the 404 -> flip-prefix troubleshooting tip. `tests/tool/test_onesig_api_tool.py` covers: the three `verify_ssl` aliases ending up as the right `ssl=` argument on `aiohttp.session.request`, cookie snapshot purity (round-trip and expired-cookie filtering), `__init__` load / `login()` save / `logout()` delete, and the persisted-cookie path bypassing the full captcha -> pubkey -> login -> account chain. Made-with: Cursor
Some providers (onesig in particular) reuse the persisted `base_url` as `default_value` on the metadata endpoint. The previous reload logic in `ServiceDetailPanel` treated `value === effectiveDefault` as a "placeholder" case and cleared the input, so reopening a configured service showed an empty API URL and saving immediately overwrote the backend record with an empty string. The form now renders whatever the backend returns under `fields` (falling back to legacy keys only) and never compares against `default_value`. `ServiceDetailPanelApi.test.tsx` adds a regression test where metadata returns the same value in both `default_value` and `fields`, asserting the form still shows the persisted URL. Made-with: Cursor
…i-integration feat/onesig api integration
…irmware Older OneSIG v2.5 builds reject RSA-OAEP ciphertext on POST /v3/login and only accept the raw password, so we ship a sibling plugin (registered as service_id `onesig_v2_5_older_api`) that mirrors the standard `onesig` handler with one targeted change: the captcha → pubkey → encrypt → login chain is collapsed to captcha → login, sending `self.config.password` in the clear. All other paths — cookie session, persistence, captcha / TOTP fallback, 401 / 1019..1022 auto-relogin, and RSA-OAEP encryption of sensitive *non-login* write fields (change_password, user_create, user_delete, audit-log purge, interface password, device-upgrade password) — are kept identical to the encrypted variant so the two can coexist on the same flocks instance without rewriting business code. Namespacing keeps the two plugins isolated end-to-end: SERVICE_ID, secret_id (`onesig_v2_5_older_password`), persisted-cookie prefix (`onesig_v2_5_older_session_cookie__`), output filename prefix, and env vars (`ONESIG_V2_5_OLDER_*`, with `ONESIG_*` retained as fallback) are all distinct, so the persisted cookie jars and credentials of the new and legacy plugins never clobber each other. The skill (`plugins/skills/onesig-use`) and existing tests are left untouched — they continue to document the standard encrypted flow. Made-with: Cursor
…ount lockout
The "Test connectivity" button looped over `service_tools[:5]` and ran
`_build_param_sets()` (4–6 enum-driven actions per tool) for each one.
For session-based services (OneSIG / OneSec / Qingteng / ...) every
failed attempt triggers a fresh login round-trip, so a single click on
wrong credentials could fire ~30 failed logins and trip the server-side
account lockout.
Changes
* Add `_is_action_dispatch_login_probe()`: classifies tools whose name
ends in `_login` and whose required `action` parameter is an enum
(e.g. `onesig_login`, `onesec_login`, `onesig_v2_5_older_login`).
* `_tool_sort_key()` now puts those at the top alongside parameter-free
login probes (priority -1).
* Replace the nested probe loop with a **single tool, single param set**
call. For action-dispatch login tools we force `action="test"`, which
the handler's `_dispatch_group` special-cases to a read-only call from
`_CONNECTIVITY_TEST_ACTIONS[group]` (onesig → `get_account`, onesec →
`common_threat_type_list`, ...). No `login` / `logout` /
`change_password` / `get_pubkey` enum value is ever invoked.
* Surface the lockout-prevention rationale in the failure message so
users know a single failed probe is not a definitive verdict.
Tradeoff: a non-auth failure on the chosen probe (e.g. a synthesized
param value the endpoint rejects) shows as a false negative; the user
re-tests after fixing config — strictly better than discovering the
service account got locked.
Tests
* Replace `test_failed_attempts_are_aggregated_in_message` (which
asserted the removed multi-attempt aggregation) with
`test_single_attempt_only_to_avoid_account_lockout`, asserting
`execute.await_count == 1` and the new "为避免连续失败导致账号锁定"
message.
* New `test_action_dispatch_login_tool_uses_test_action` pins the
contract that `onesig_login`-style tools are picked over their
sibling `_assets` / `_monitoring` groups and invoked with exactly
`{"action": "test"}`.
Made-with: Cursor
P0 — 与设计承诺对齐
* routes/device.py 中 4 处内联 SQL(INSERT/UPDATE/DELETE/test 结果写回)
全部下沉到 store.py 的 insert_device / update_device_row /
delete_device_row / record_test_result
* secrets.py 抽出 _parse_placeholder helper,消除 delete_secrets /
mask_for_display / resolve_for_runtime 三处重复的 {secret:...}
解析逻辑
P1 — 可读性与一致性
* store.create_group 在同一连接内 INSERT + SELECT,去掉 type: ignore
* store.list_devices 改用 async with cur 风格,与其他函数统一
* store 新增 _now_ms() helper,13 处时间戳代码集中
* routes 中 new_name 表达式拆分,body.name.strip() 只求值一次
* routes._probe 抽出连通性探测,主路由更聚焦
P2 — 小毛病
* route_update_group 在 IntegrityError 时改显示 "(unchanged)",
避免出现 "机房名称 'None' 已存在"
* 全部日志服务名前缀统一为 tool.device.*
migration.py 也复用新 insert_device helper,并将值提取/插入逻辑
拆分为 _extract_plain_values / _insert_migrated_device 两个内部函数,
主流程从 100+ 行缩到 50 行内。
Co-authored-by: Cursor <cursoragent@cursor.com>
* routes/device.py 删除未使用的 log 实例 * routes/device.py _probe 中 elapsed() 由 lambda 改为嵌套 def(PEP 8 E731) * migration.py 在循环内抽出 now 变量,消除两处重复 int(time.time()*1000) * secrets.py / migration.py 为反向依赖(domain → transport import flocks.server.routes.provider 的私有 helper)加 TODO 注释, 说明这些 schema helper 应迁出 routes,下个迭代单独处理 Co-authored-by: Cursor <cursoragent@cursor.com>
…verse dependency
将 13 个与 HTTP 无关的 helper 从 flocks/server/routes/provider.py(transport
层)迁出,集中到新的领域模块:
flocks/tool/api_service/
__init__.py
schema.py — APIServiceCredentialField + 13 个 helper
变更点
------
1. flocks/tool/api_service/schema.py 新增,包含:
* APIServiceCredentialField (Pydantic 模型)
* _default_api_service_field_label / _extract_secret_id
* _get_compound_secret_metadata / _should_persist_secondary_secret
* _load_provider_yaml_metadata / _load_api_service_metadata_data
* _normalize_api_service_credential_field / _build_api_service_credential_schema
* _get_api_service_schema_field / _get_api_service_secret_field_names
* _get_api_service_default_secret_id / _get_api_service_secret_candidates
清理:legacy metadata 目录用 .resolve().parents[2] 计算,更稳健
2. flocks/server/routes/provider.py 减少 ~360 行:
* 删除上述全部原定义
* 顶部 import 重新导出,保持向后兼容(tests/onboarding/skill 仍可
from flocks.server.routes.provider import ...)
3. flocks/tool/device/secrets.py / migration.py:
* 改为 from flocks.tool.api_service.schema import …
* 去除 TODO(arch) 反向依赖注释
效果
----
* domain layer (flocks.tool.device) 不再依赖 transport (server.routes)
* schema 模块作为单一信源,provider.py 是 thin HTTP wrapper
* provider.py 由 2907 行降至 2554 行
Co-authored-by: Cursor <cursoragent@cursor.com>
P0 — 实际问题
* provider.py 删除 dead import json(_load_api_service_metadata_data
迁出后不再使用)
* provider.py 顶部 import 注释从两行 import 之间挪到 import 块前一空行,
保持 import 块连续性
P1 — 风格 / DRY
* schema.py 将 _load_provider_yaml_metadata 函数体内的 import 提到
文件顶部(yaml、discover_api_service_descriptors)
* schema.py _LEGACY_METADATA_DIR 由 .resolve().parents[2]/"tool"/...
改为 .resolve().parent.parent/"security"/... 兄弟目录写法,
一眼可读
* device/secrets.py _parse_placeholder 改为 schema._extract_secret_id
的 import alias,消除两个文件中 99% 重复的解析代码
注:provider.py 内部仍有一处 Path.home() 使用,因此 pathlib.Path 保留。
Co-authored-by: Cursor <cursoragent@cursor.com>
…o api_service_schema.py flocks/tool/api_service/schema.py → flocks/tool/schema/api_service_schema.py - 目录名 schema/ 更直接表达这是 schema 定义层 - 文件名 api_service_schema.py 比 schema.py 更具体,避免二义性 - 同步更新 provider.py / device/secrets.py / device/migration.py 的 import 路径 Co-authored-by: Cursor <cursoragent@cursor.com>
…instance devices
通过 Python contextvars 在协程级别注入凭据,无需克隆任何插件文件,
即可让同一型号的多台设备(如两台 TDP)对 Agent 完全独立可寻址。
核心变更:
- 新增 flocks/tool/credential_context.py:零依赖 ContextVar 模块
* activate_device_credentials(device_id) 异步上下文管理器
* 读取 DB 凭据 + _provider.yaml credential_fields 映射,分别构建
secret_ovr(供 SecretManager)和 config_ovr(供 ConfigWriter)
* config_ovr 中 secret 字段以 {secret:sid} 占位符形式写入,使
handler 的 _resolve_ref() 调用链完整可用
* service_id 精确匹配,防止跨服务污染
- flocks/security/secrets.py:SecretManager.get() 先查 device override
- flocks/config/config_writer.py:get_api_service_raw() 先查 device override
- flocks/tool/registry.py:execute() 检测 device_id kwarg 并激活 override;
设备禁用时返回明确错误而非静默回退全局凭据
- 新增 flocks/tool/device/device_context_tool.py:Agent 可调用的内置工具,
返回机房 → 设备 → 工具层级结构(含每台设备的 device_id)
- 新增 flocks/tool/device/prompt.py:构建结构化设备上下文,展示
device_id 及调用示例供 Agent 参考
- flocks/tool/device/sync.py:改为按 storage_key 汇总 enabled 状态,
OR 逻辑确保同型号设备独立启用/禁用
- flocks/tool/device/store.py:新增 device_revision 版本计数器,用于
session runner 系统 prompt 缓存失效
- flocks/session/runner.py:系统 prompt 改为轻量 hint,详细上下文按需
通过 device_context 工具获取,避免与工具目录重复
- flocks/server/routes/device.py:回退至简洁路由层,移除 provisioner 调用
- webui/src/pages/DeviceIntegration/index.tsx:向导式添加设备 UI
Agent 调用示例:
tdp_event_list(action="list", device_id="abc-123") # 总部 TDP
tdp_event_list(action="list", device_id="def-456") # 分支 TDP
Co-authored-by: Cursor <cursoragent@cursor.com>
…vice integration UX
**后端**
- tool_loader: 新增 TOOL_TYPE_DEVICE / API_LIKE_SOURCES 常量,从 tools/device/ 目录
推断 source="device" 并从 _provider.yaml 读取 vendor 字段
- registry: ToolInfo 增加 vendor 字段;device 工具 schema 自动注入 device_id 参数;
execute() 在未传 device_id 时自动解析唯一 enabled 设备并激活凭据上下文
- credential_context: activate_device_credentials 将 verify_ssl / ssl_verify 注入
config_ovr,暴露 get_verify_ssl_override() ContextVar 供 HTTP handler 读取
- tool_loader: 内置 YAML HTTP handler 通过 ContextVar 读取 verify_ssl 并传给
aiohttp TCPConnector,修复开关无效问题
- provider/tool routes: source 判断扩展为 API_LIKE_SOURCES;APIServiceSummary /
ToolInfoResponse 新增 vendor 字段
- api_service_schema: _load_provider_yaml_metadata 返回 vendor 字段
- device migration: 增加 service_id 去重检查,防止 api_versioning 重命名
storage_key 后重复迁移产生冗余设备行
- device/test 接口: 支持 base_url / verify_ssl 覆盖参数,允许前端用当前表单值探测
**目录迁移**
- 将 ngsoc/ngtip/onesec/onesig/qingteng/sangfor_af|sip|xdr/skyeye/tdp 等所有设备
插件从 tools/api/ 迁移到 tools/device/(包括 .flocks/plugins 与 flockshub)
- 各 _provider.yaml 注入对应的 vendor 字段(threatbook/qianxin/sangfor/qingteng)
- installer: _TOOL_TYPE_DIRS 增加 "device";catalog: 跳过 device/ 目录条目;
api_versioning: _api_plugin_roots 扫描 tools/api/ 和 tools/device/
**前端**
- 移除硬编码 VENDORS 数组和 getVendor;改为 VENDOR_PRESENTATION + vendorPresentation
动态映射,直接消费后端 APIServiceSummary.vendor 字段
- 添加设备向导:availableVendors 按 vendor 字段动态推导,threatbook 置顶排序
- vendorByKey 索引同时收录版本化 storage_key 和裸 service_id(去版本后缀),
修复旧迁移设备(无版本 storage_key)无法显示厂商标签的问题
- SSL 开关即时持久化:切换时立刻调 PUT /api/devices/{id} 写库,失败则回滚;
测试连接携带表单当前 verify_ssl / base_url,无需先保存
Co-authored-by: Cursor <cursoragent@cursor.com>
…ry page
核心功能
- 新增 Skill.toggle_disabled / set_disabled / forget_disabled / rename_disabled 等
禁用状态管理方法,持久化至 ~/.flocks/config/skill_settings.json
- 新增 Skill.list_enabled():仅返回已启用技能,供 agent 运行时使用
- 新增 PATCH /api/skills/{name}/toggle 端点,返回新 disabled 状态
- skill_settings.json 写入采用 tmpfile + os.replace 原子写,防止崩溃时
文件损坏导致禁用偏好静默丢失
- 使用 threading.RLock + fcntl/msvcrt 跨进程文件锁保护 RMW 操作,
兼容 uvicorn --workers N 多进程部署
Agent 侧保护
- agent/registry.py、session/runner.py、tool/system/skill.py、
tool/agent/delegate_task.py、tool/system/slash_command.py 全部切换为
Skill.list_enabled(),禁用 skill 不再注入 system prompt
- tool/system/skill.py skill_tool_impl 对禁用 skill 返回错误,拒绝加载
- server/routes/misc.py 的 SDK/TUI 兼容端点(GET /skill、GET /skill/{name})
同步过滤禁用 skill,避免 TUI-attached agent 绕过开关
command/handler.py /skills list 保留 Skill.all(),但在禁用技能后附加
[disabled] 标注,让用户在 CLI 视图中仍能看到完整列表
API 鉴权
- 所有写端点(create / update / delete / toggle / install / refresh)
显式添加 Depends(require_user) 作为第二层防御,全局 auth_middleware
为第一层
WebUI 重设计(webui/src/pages/Skill/index.tsx)
- 从卡片 Grid 改为 table-fixed 列表视图(5 列:类型/名称/来源/启用/操作)
- 操作列新增编辑按钮(Pencil),行本身不再可点击;删除按钮移入 SkillSheet
- 新增 FilterChips 段控制器:全部 N · 已启用 N · 已禁用 N,同时充当
统计展示与视图过滤,点击即过滤
- 删除独立"状态"列;缺依赖警告内嵌至名称列下方,仅缺依赖时显示
- 安装/创建按钮从 PageHeader action 移至 toolbar,与搜索/过滤/刷新同行
- PaginationBar 改用 i18n 键;page state 与 totalPages 保持同步
i18n(zh-CN / en-US skill.json)
- 新增 table.edit、filter.*、eligibility.missingDepsInline、pagination.info
- en-US totalCount 切换为 _one/_other 复数形式修复"1 skills"问题
Co-authored-by: Cursor <cursoragent@cursor.com>
…and filter pagination
视觉优化:
- 每张 Agent 卡片顶部加彩色细条 + 配色 Avatar,颜色由 agent.color
优先,无则按名称哈希取调色板,各 Agent 视觉可辨
- Model 信息改为彩色圆形 chip,背景与边框继承 Agent 主色
- Section header 加左侧 border-l-2 色条,副标题独立一行
- 编辑按钮从纯图标改为"图标 + 文字"小按钮
来源区分(内置 vs 自定义):
- native=true(flocks/agent/agents/ 及项目级插件)→ 蓝色"内置"badge
- native=false(~/.flocks/plugins/agents/)→ 青绿色"自定义"badge
- 每张卡片始终显示来源 badge
删除按钮:
- 卡片 footer 新增删除按钮(左侧),编辑按钮保持右侧
- 内置 Agent(native=true)删除按钮置灰 + disabled,悬停提示"内置 Agent 不支持删除"
- 自定义 Agent 删除按钮正常,hover 变红
子 Agent 筛选与分页:
- 子 Agent section 增加来源筛选器,样式与 Skill 页 FilterChips 完全一致:
segmented control + 各选项实时数量徽标(全部 / 内置 / 自定义)
- 每页最多 12 个(xl:4列×3行 / lg:3列×4行),超出时显示分页条
- 切换筛选自动回到第 1 页,page 超出总页数时自动收缩
- 网格容器设 minHeight: 540px,切换筛选时内容区高度保持稳定,
消除内容塌陷引起的视觉跳动
i18n:
- agent.json 新增 badge.custom、badge.nativeDeleteDisabled、
filter.{all,builtin,custom,aria}、pagination.info 键
Co-authored-by: Cursor <cursoragent@cursor.com>
…agement section to system center 新增 SystemLog 独立页面: - 新建 pages/SystemLog/index.tsx,将 LogViewerModal 的功能完整迁移为全页面布局 - 页面包含 PageHeader(标题"系统日志"、ScrollText 图标)、工具栏(文件选择器 / 自动滚动开关 / 刷新按钮)、全高度终端样式日志内容区、底部文件元信息栏 - 注册路由 /system-logs(懒加载),放置于系统中心分组下 导航重构: - 侧边栏导航分组名「管理」→「系统中心」(Layout.tsx + nav.json 双语) - 在「账号管理」下方新增「系统日志」导航项(/system-logs,ScrollText 图标) 工具页清理: - 移除 Tool 页的 LogViewerModal import、showLogViewer state、日志按钮及 Modal 渲染 - Tool 页工具栏不再包含日志入口,统一由侧边栏导航访问 i18n: - nav.json 新增 systemCenter、systemLog 键(中英双语) - tool.json logs 组新增 description 键(页面副标题) Co-authored-by: Cursor <cursoragent@cursor.com>
…ice enable toggle
按钮颜色统一:
- 顶部工具栏刷新按钮改为纯图标样式(移除"刷新"文字,加 title tooltip),
与其他页面刷新按钮风格一致
- 顶部"添加设备"按钮 bg-blue-600 → bg-red-600,与全站主操作按钮统一
- 表单"保存配置"按钮 bg-blue-600 → bg-red-600,保持一致性
启用设备开关即时生效(复用 SSL 验证同款模式):
- DeviceConfigPanel 新增 onToggleEnabled prop
- 内部 handleToggleEnabled:乐观更新 → 调用 onToggleEnabled(next) →
成功 toast("设备已启用"/"设备已停用")→ 失败回滚并提示"保存失败,已回滚"
- 新增设备时(尚未入库)仅更新本地状态,等"确认接入"一起提交
- 父组件 handleToggleEnabled 调用 deviceAPI.update({ enabled }) 后
re-fetch 刷新面板与列表,UI 始终与后端保持同步
Co-authored-by: Cursor <cursoragent@cursor.com>
…r and fix sidebar home label - 侧边栏「Flocks 首页」改为「首页」(中英文 nav.json 同步) - 工作流卡片改为 Agent 页面风格:顶部彩色 accent 条、白底卡片、 彩色 avatar、来源徽章(内置/自定义),保留调用次数/成功率/平均耗时统计 - 新增工具栏:分段筛选(全部/内置/自定义)+ 刷新 + 创建按钮, 与 Skill/Agent 页面交互风格保持一致 - 区头采用 border-l-2 accent 样式,超过 12 项自动分页 - workflow.json 补充 filter.*、badge.*、totalCount 等 i18n 键 Co-authored-by: Cursor <cursoragent@cursor.com>
… of sparkles Co-authored-by: Cursor <cursoragent@cursor.com>
…eraction style - MCP/API/本地工具页改为 CSS Grid 列对齐列表(去除卡片网格) - 统一列结构:图标 | 名称+描述 | 类型 | 状态 | 统计 | 操作,所有行严格对齐 - 移除各列表行左侧彩色状态条 - 列表行不再整行可点击,仅「管理」按钮触发详情/编辑面板 - 无 GitHub 链接的行补透明占位保持按钮列宽一致 - 全量工具页去掉工具描述行、测试/详情链接,操作列统一为「管理」按钮 - MCP 页删除重复的「刷新状态」按钮 - SortFilterHeader 支持 asDiv prop,可在 div 容器内复用 Co-authored-by: Cursor <cursoragent@cursor.com>
… column - Replace toolbar status chips (all/enabled/disabled) with source chips (all/built-in/custom) - Add multi-select column filter dropdowns to Source and Enabled headers, matching the Tool list SortFilterHeader style (red funnel icon when active, checkbox menu, clear-filter link at top) - Always render table header and filter controls when inventory is non-empty; show inline empty-row with a "Clear filters" button instead of replacing the table - Fix empty-state copy to distinguish between search-no-results and filter-no-results - Center-align the Actions column header and manage button in the All-tools list Co-authored-by: Cursor <cursoragent@cursor.com>
4236c6b to
cfeba02
Compare
…ebar_and_add_shortcuts - Accept renamed versioned tool plugin dirs (ngtip_v5_1_5, onesec_v2_8_2, etc.) - Accept upstream device plugin dirs added to tools/device/ - Keep device integration features: startup phase, router, credential override, integration_type/vendor fields, device source in registry - Keep system center navigation, system-log route and skill toggle auth - Keep schema refactor (helpers moved to flocks/tool/schema/api_service_schema.py) Co-authored-by: Cursor <cursoragent@cursor.com>
…o device/ Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…er.yaml files Co-authored-by: Cursor <cursoragent@cursor.com>
- skill: block disabled skills from `delegate_task(load_skills=...)` so the toggle in the Skills UI can no longer be bypassed via subagent injection. Disabled skills are now reported as "not found" to avoid signalling their existence to the LLM. - device: persist `enabled=false` for storage_keys whose last device instance was just removed. `sync_service_tool_state` now accepts a `deleted_storage_keys` hint (used by `route_delete_device`) and also sweeps the api_services config for entries whose backing devices no longer exist, so historical dirty state self-heals on next sync. The startup `_sync_all` additionally walks api_services to pick up service_ids that have zero remaining DB rows. Idempotent: writes are skipped when the config already matches. - agent: invalidate the per-worker agent cache when another uvicorn worker toggles a skill. `Agent.state` now checks the mtime of `skill_settings.json` before serving cached prompts, so a PATCH from one worker is observed across all workers without IPC. Co-authored-by: Cursor <cursoragent@cursor.com>
Each chat part was wrapped in its own `<div key>`, so the thinking block's `mt-2 first:mt-0` always saw itself as the first child of its wrapper and collapsed the top gap to zero. The result was that a tool card followed by a thinking block (or vice versa) looked glued together, while two consecutive thinkings or two consecutive tools still had 8px between them — visibly uneven spacing. Move the inter-part gap to the wrapper itself (`mt-2 first:mt-0`) and strip the redundant `mt-2` from `ChatToolPart` (`<details>` + waiting- for-answer branch) and the thinking block. With a single source of truth at the wrapper level, every adjacent pair of parts now has a uniform 8px gap and the first part still sits flush with the message header. Co-authored-by: Cursor <cursoragent@cursor.com>
…ebar_and_add_shortcuts
Merge `origin/dev` into the PR 131 sidebar/shortcuts branch. Nine files
had textual conflicts; their resolution preserves PR 131's disabled-skill
fixes and UI redesign while picking up dev's slash-command refactor and
skill page throttled refresh.
Conflict resolutions (9 files):
- flocks/command/handler.py: drop legacy 240-line if-chain; route every
direct slash command through `run_direct_command` (dev refactor).
- flocks/command/direct.py (non-conflict, paired change): port PR 131's
user-vs-agent visibility split to the new entry point. `/skills`
shows the full inventory with `[disabled]` markers when called from a
user surface (CLI/TUI/WebUI) and stays "enabled-only" when invoked by
the agent via the `slash_command` tool.
- flocks/session/runner.py: drop the obsolete `_build_system_prompts`
method (now centralised in `SessionPrompt.build_system_prompts` on
dev); migrate the PR 131 device-asset hint into a small
`_build_device_asset_hint` helper, appended after the cached prompt
list so live device state never pollutes the prompt cache. Keep
PR 131's per-turn `skill` tool description refresh in the schema
builder so disabled skills cannot leak into the tool index.
- flocks/tool/system/skill.py: keep PR 131's enabled-only description
refresh in the wrapper; merge dev's clarifying docstring.
- flocks/tool/system/slash_command.py: drop inlined `/skills`/`/workflows`
handlers in favour of `run_direct_command` (dev refactor).
- webui/src/pages/Tool/index.tsx: use the narrower `refreshToolData()`
on enabled-toggle (dev), avoiding redundant MCP refreshes.
- webui/src/pages/Session/index.tsx: restore dev's
`readLastSelectedSessionId` effect so the existing
`writeLastSelectedSessionId` writer has a matching reader.
- webui/src/pages/Skill/index.tsx: union of both sides — keep PR 131's
pagination/source-filter/toggle state and lucide icons, adopt dev's
throttled `refreshSkillsAndFetch` (already referenced downstream),
and fix one stray `toast.error` → `showErrorToast` left over from the
rename.
- webui/src/pages/Workflow/index.tsx: keep PR 131's toolbar-based
refresh/create actions and `WorkflowSection` component; drop dev's
undefined `isUserManaged` helper in favour of the still-defined
`isBuiltin`.
- webui/src/components/common/SessionChat.tsx: keep PR 131's redesigned
tool card (status pill + `ChevronDown` + dark code block) and absorb
dev's `truncateToolDisplayText`/`buildToolInputSummary` helpers plus
hover-title fallback for long inputs.
Hidden semantic conflicts (silently auto-merged but failing tests):
- webui/src/components/common/SessionChat.test.ts: update className
assertions — PR 131 moved `max-w-2xl` from the inner bubble to the
outer `max-w-[50%]` container, so the inner bubble now only owns
`w-auto`/`w-full`.
- webui/src/pages/Workflow/index.tsx (a11y): wrap each `WorkflowSection`
in `<section aria-label={title}>` so the dev-side region-role tests
keep passing without rolling back the redesign.
Pre-existing PR 131 debt also tidied up in this commit:
- webui/src/pages/Tool/ToolDetailDrawer.test.tsx: add the missing
`listFixtures` stub (the new fixtures effect was added to
`Tool/index.tsx` without a matching mock).
- webui/src/pages/Skill/SkillSheet.test.tsx: change the Edit-mode
fixture to `source: 'user'` so the editable code path is exercised.
`source: 'project'` skills remain read-only on purpose to prevent
the UI from overwriting repo-tracked files; the read-only branch is
still covered by `should show name field in edit mode`.
Verification: 192/192 webui vitests pass; `tsc --noEmit` clean;
`py_compile` clean on all five touched Python modules.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Skill list: make the entire name+description+icon block clickable to open SkillSheet, matching the Hub catalog row pattern - Tool list (all tabs): make name+description blocks clickable links; use group/name scoped hover colour per tab accent (slate/blue/purple) - MCP / API / Local tabs: replace fixed `minmax(0,1fr)` name column with proportional `minmax(min, Xfr)` grid so excess width is shared across all columns instead of collecting as a single gap after the name; extract shared constant to `components/gridLayout.ts` so all three tabs stay in sync - ToolTable (all-tools tab): same proportional grid treatment; name capped at 420 px, font reduced one step (text-xs) to suit the denser row - SessionChat user bubble footer: wrap bubble+footer in a `flex flex-col` container so the footer stretches to the bubble's intrinsic width, keeping the timestamp on the bubble's left edge and action buttons on the right edge regardless of message length Co-authored-by: Cursor <cursoragent@cursor.com>
修改左侧边栏,根据使用频率和场景调整菜单顺序
修改版本信息放置文字和中英文切换按钮
调整边栏宽度使其更美观
增加 Alt+N 快捷键
增加 /new 快捷键