diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9672bb353..2b6aaa207 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug Report -about: 报告一个问题 +about: 报告小安桌面端的问题 title: "[Bug] " labels: bug assignees: "" @@ -9,7 +9,8 @@ assignees: "" **环境信息** - OS: - Node.js 版本: -- HanaAgent 版本: +- 小安版本 / commit: +- 数据目录:`~/.xiaoan` / `XIAOAN_HOME` / legacy `~/.hanako` **问题描述** 简明描述遇到的问题。 @@ -25,3 +26,8 @@ assignees: "" **日志 / 截图** 如有相关日志或截图请贴出。 + +**已尝试** +- [ ] 重启小安 +- [ ] 使用开发数据目录复现 +- [ ] 查看终端或 diagnostics 日志 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..054d75959 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 维护计划 + url: https://github.com/An-an-525/An-Agent/blob/main/docs/PROJECT-MAINTENANCE-PLAN.md + about: 查看当前 PR 拆分、合并门禁和维护队列。 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2cdb78313..bb6562388 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,6 @@ --- name: Feature Request -about: 提一个新功能建议 +about: 提一个小安产品能力建议 title: "[Feature] " labels: enhancement assignees: "" @@ -12,5 +12,13 @@ assignees: "" **使用场景** 这个功能解决什么问题?在什么场景下会用到? +**用户路径** +用户第一次看到它时,应该从哪里进入?需要哪些默认状态? + +**边界** +- [ ] 不需要替换 Pi SDK +- [ ] 不需要自研 agent loop +- [ ] 不破坏 legacy 数据读取 + **可能的实现方式** -如果有想法,描述一下可能的实现路径。 +如果有想法,描述一下可能的实现路径;没有也可以留空。 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..1ca119281 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,55 @@ +## Summary + +- + + + +## Layer + +- [ ] L5 product / docs / copy +- [ ] L4 desktop / server / hub shell +- [ ] L3 core domain +- [ ] L2 lib / shared +- [ ] L0 Pi SDK + +## Change Type + +- [ ] Xiaoan product copy / brand +- [ ] Runtime migration / config +- [ ] UI layout / theme / motion +- [ ] Provider / model defaults +- [ ] Plugin / capability +- [ ] Bug fix +- [ ] Tests only +- [ ] Docs only + +## Guardrails + +- [ ] No Pi SDK replacement or custom agent loop +- [ ] No unrelated refactor bundled in +- [ ] Legacy compatibility preserved where relevant +- [ ] User/provider config is not overwritten +- [ ] User-visible copy avoids upstream/framework terms unless intentionally internal +- [ ] Docs updated or not needed + +## Verification + +- [ ] `npm run typecheck` +- [ ] `npm run dep:check` if cross-layer imports changed +- [ ] Relevant focused tests: + +```text + +``` + +- [ ] Full `npm test` for large/release-bound batches +- [ ] Desktop smoke/manual check if UI changed + +## Risk / Rollback + +- Risk: +- Rollback: + +## References / Credits + +- Open-source projects or upstream changes referenced: diff --git a/.gitignore b/.gitignore index eef308421..4e542ccb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # ═══════════════════════════════════════════ -# 用户数据(已迁移到 ~/.hanako/) +# 用户数据(canonical ~/.xiaoan/;legacy ~/.hanako/ 双读) # ═══════════════════════════════════════════ agents/ user/ @@ -61,6 +61,7 @@ CLAUDE.md # 临时文件 tmp/ test-results/ +.vitest-results*.json __pycache__/ *.pyc /index.html diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..a45fd52cc --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8024ccd5a..40661f5b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ -# Contributing to 小安 Agent(An-Agent) +# Contributing to 小安(Xiaoan) 感谢参与 **[An-an-525/An-Agent](https://github.com/An-an-525/An-Agent)** 的主线维护。 -本仓库是基于 [OpenHanako](https://github.com/liliMozi/openhanako) 的 **独立 fork**,不是上游官方仓库。贡献前请先读 [docs/README.md](./docs/README.md) 了解文档结构。 +本仓库维护的是 **小安桌面 AI 助手**。贡献前请先读 [docs/README.md](./docs/README.md) 了解文档结构;技术来源与开源参考放在文末说明,不要在 GitHub 标题、PR 标题和首屏描述里先讲上游项目。 --- @@ -10,8 +10,8 @@ | 问题 | 答案 | |------|------| -| 这是什么? | 通用桌面个人 Agent(左聊右看),品牌「小安 / Xiaoan」 | -| 底座能换吗? | **不能** — 运行时 OpenHanako + Pi SDK | +| 这是什么? | 开箱即用的桌面 AI 助手(左聊右看),品牌「小安 / Xiaoan」 | +| 底座能换吗? | **不能** — 保持现有桌面壳与 Pi SDK 底座 | | 主要改哪里? | `plugins/xiaoan-core`、`desktop/` 定制 UI、`shared/xiaoan-*`、`docs/` | | 什么不做? | PPT 生成线、自研 agent loop、fork Pi 内核 | | 工程原则? | 不换底座 → 最小 diff → 分层改动 → 可验证 → 可合并上游 | @@ -19,6 +19,8 @@ 详细方法论:[docs/XIAOAN-ENGINEERING-METHODOLOGY.md](./docs/XIAOAN-ENGINEERING-METHODOLOGY.md) +当前大改动收口、PR 拆分和合并门禁见:[docs/PROJECT-MAINTENANCE-PLAN.md](./docs/PROJECT-MAINTENANCE-PLAN.md)。不要把多轮 Cursor/历史改动整包提交。 + --- ## 2. 开发环境 @@ -44,7 +46,7 @@ npm install ### 数据隔离(推荐) ```bash -export HANA_HOME=~/.hanako-dev +export XIAOAN_HOME=~/.xiaoan-dev ``` ### 启动 @@ -68,13 +70,13 @@ npm start ``` L4 产品/品牌/docs shared/xiaoan-* · themes · design · locales L3 小安方法论 plugins/xiaoan-core · skills2set/xiaoan-handbook -L2 OpenHanako 继承 server · core · hub · lib · 大部分 desktop/react +L2 继承底座 server · core · hub · lib · 大部分 desktop/react L1 Pi SDK lib/pi-sdk/(几乎不改) ``` | 需求类型 | 首选路径 | |----------|----------| -| 新 Agent 工具 | `plugins/xiaoan-core/tools/` | +| 新小安能力工具 | `plugins/xiaoan-core/tools/` | | Brief / Plan 右栏 | `desktop/src/react/components/right-workspace/` | | 控制台排版 | `desktop/src/design/xiaoan-layout.css` | | 主题 / 品牌 | `desktop/src/themes/xiaoan-console*.css` | @@ -104,6 +106,7 @@ npm run dep:check | 迁移 / first-run | `npm test -- tests/xiaoan-fork-migrate.test.ts` | | 聊天排版 | `npm test -- InputArea.layout` | | 主题 | `npm test -- tests/theme-registry.test.ts` | +| 桌面 UI 契约(legacy theme / GSAP) | `npm test -- tests/xiaoan-layout-theme-contract.test.ts tests/xiaoan-gsap-chat-motion.test.ts` | UI 改动请在本地目测:`localStorage.setItem('xiaoan-theme','xiaoan-console'); location.reload()` @@ -113,12 +116,18 @@ UI 改动请在本地目测:`localStorage.setItem('xiaoan-theme','xiaoan-conso ### Commit message +GitHub 展示规则: + +- commit / PR 标题先讲小安结果,不用上游项目名开头。 +- 用户可见变更用“小安、能力、工作区、状态、连接”等产品语言。 +- 上游和开源来源放在 PR 末尾的 reference / credit,不放在 summary 第一屏。 + ``` (): -feat(ui): add flex-footer layout for xiaoan-console -fix(xiaoan-core): guard brief gaps to max 3 -docs: update layout system guide +feat(ui): stabilize Xiaoan chat layout +fix(runtime): preserve Xiaoan provider config during migration +docs(github): align Xiaoan project presentation ``` ### PR 描述建议包含 @@ -128,6 +137,8 @@ docs: update layout system guide - **Test plan**:跑过哪些命令、手动检查项 - **Docs**:更新了哪些 `docs/` 文件 +GitHub 会自动使用 `.github/pull_request_template.md`。提交前按模板确认:单一意图、分层明确、legacy 兼容未破坏、相关测试已跑。 + ### 不要提交 - `.env`、API Key、`~/.hanako` 用户数据 @@ -141,6 +152,8 @@ docs: update layout system guide - 新能力 → 对应 CODEMAP 专题 + [docs/README.md](./docs/README.md) 若需登记 - 排版 → [docs/XIAOAN-LAYOUT.md](./docs/XIAOAN-LAYOUT.md) +- 主题 / GSAP / 桌面 UI 规范 → [docs/XIAOAN-DESKTOP-UI-STANDARDS.md](./docs/XIAOAN-DESKTOP-UI-STANDARDS.md) +- UI bug 事件记录 → [docs/incidents/](./docs/incidents/README.md)(黑曜石:`Obsidian/初始/99 - Agent指南与规范/An-Agent/`) - 维护流程 → [docs/MAINTAINER.md](./docs/MAINTAINER.md) --- @@ -151,10 +164,10 @@ Issue 请包含: - OS 与版本 - Node.js 版本 -- `HANA_HOME` 是否隔离 +- `XIAOAN_HOME` 是否隔离 - 复现步骤 - 期望 vs 实际 -- 相关日志(`~/.hanako-dev/diagnostics/` 或终端输出) +- 相关日志(`~/.xiaoan-dev/diagnostics/`、legacy `~/.hanako-dev/diagnostics/` 或终端输出) --- @@ -167,7 +180,7 @@ desktop/ Electron + React(含 xiaoan-console UI) hub/ 调度、频道、bridge lib/ 记忆、工具、沙盒 plugins/ 内置插件(xiaoan-core) -skills2set/ 内置 Skill(xiaoan-handbook) +skills2set/ 内置技能(xiaoan-handbook) shared/ schema、xiaoan-fork 元数据 tests/ Vitest docs/ 说明与工程方法论(从 docs/README.md 进入) @@ -189,7 +202,9 @@ docs/ 说明与工程方法论(从 docs/README.md 进入) | 文档 | 用途 | |------|------| | [docs/README.md](./docs/README.md) | 文档总索引 | +| **黑曜石** `99 - Agent指南与规范/An-Agent/00-An-Agent-工程文档索引.md` | 维护者知识库(含全部代码地图镜像) | +| `./scripts/sync-docs-to-obsidian.sh` | 将 docs/ 同步到黑曜石 | | [docs/XIAOAN-ENGINEERING-METHODOLOGY.md](./docs/XIAOAN-ENGINEERING-METHODOLOGY.md) | 工程方法论 | | [docs/MAINTAINER.md](./docs/MAINTAINER.md) | 维护者手册 | -| [docs/HANAKO-INTERNALIZATION.md](./docs/HANAKO-INTERNALIZATION.md) | OpenHanako 内化 | -| [docs/agent-design/XIAOAN-METHODOLOGY.md](./docs/agent-design/XIAOAN-METHODOLOGY.md) | Agent 方法论 | +| [docs/agent-design/XIAOAN-METHODOLOGY.md](./docs/agent-design/XIAOAN-METHODOLOGY.md) | 小安方法论 | +| [UPSTREAM-NOTICE.md](./UPSTREAM-NOTICE.md) | 技术来源与开源参考 | diff --git a/README.md b/README.md index 422177045..2825ee3cc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# 小安 Agent(Xiaoan) +# 小安(Xiaoan) **主维护仓库**:[An-an-525/An-Agent](https://github.com/An-an-525/An-Agent) · 默认分支 `main` · 维护者 **An-an-525** -基于 [OpenHanako](https://github.com/liliMozi/openhanako) fork 的通用桌面个人 Agent,运行时底座为 **Pi SDK**。本仓库是**独立产品线**的日常开发与发布入口,不接收上游 OpenHanako 自动更新。 +小安是开箱即用的桌面 AI 助手:左侧聊天,右侧查看资料和工作区,内置记忆、工具、能力扩展和书桌能力。用户打开应用看到的是完整的小安产品,而不是需要先配置的通用 AI 平台。 > 不做 PPT 生成线。小安是左聊右看的 AI 副驾驶:对话、工具、记忆、书桌工作区。 @@ -12,31 +12,19 @@ |---|---| | **Canonical 仓库** | `https://github.com/An-an-525/An-Agent` | | **开发分支** | `main`(已合并 xiaoan-console 排版与品牌化) | -| **上游底座** | OpenHanako — 仅作源码参考,手动 merge/cherry-pick | | **配置真源** | `shared/xiaoan-fork.json` | -| **本地数据** | 生产 `~/.hanako` · 开发 `HANA_HOME=~/.hanako-dev` | +| **本地数据** | 生产 `~/.xiaoan`(legacy `~/.hanako` 双读)· 开发 `XIAOAN_HOME=~/.xiaoan-dev` | 新功能、UI、文档、插件改动 **一律在本仓库 `main` 维护**;见 [docs/FORK-CONFIG.md](./docs/FORK-CONFIG.md)。 -## 与上游关系 - -| 项 | 说明 | -|---|---| -| **上游** | [liliMozi/openhanako](https://github.com/liliMozi/openhanako)(Apache 2.0) | -| **底座** | Pi coding-agent | -| **定制** | `plugins/xiaoan-core/` — think / brief / ask + 默认「小安」Agent 模板 | -| **设计文档** | `docs/agent-design/` | - -详见 [UPSTREAM-NOTICE.md](./UPSTREAM-NOTICE.md) 与 [docs/FORK-CONFIG.md](./docs/FORK-CONFIG.md)。 - -**建议阅读顺序**: [docs/CODEMAP.md](./docs/CODEMAP.md)(导航)→ [docs/HANAKO-INTERNALIZATION.md](./docs/HANAKO-INTERNALIZATION.md)(内化底座)→ [docs/LOCAL-DEV.md](./docs/LOCAL-DEV.md)(跑起来)→ [docs/XIAOAN-LAYOUT.md](./docs/XIAOAN-LAYOUT.md)(控制台排版)→ [docs/agent-design/XIAOAN-METHODOLOGY.md](./docs/agent-design/XIAOAN-METHODOLOGY.md)(小安层)。 +**建议阅读顺序**: [docs/README.md](./docs/README.md)(文档中心)→ [docs/LOCAL-DEV.md](./docs/LOCAL-DEV.md)(跑起来)→ [docs/CODEMAP.md](./docs/CODEMAP.md)(导航)→ [docs/XIAOAN-LAYOUT.md](./docs/XIAOAN-LAYOUT.md)(控制台排版)→ [docs/agent-design/XIAOAN-METHODOLOGY.md](./docs/agent-design/XIAOAN-METHODOLOGY.md)(小安方法论)。 **本地看桌面端** → [docs/LOCAL-DEV.md](./docs/LOCAL-DEV.md) -## 功能(继承自 OpenHanako) +## 功能 -- 记忆、人格、书桌(工作区)、多 Agent -- Skills、插件、沙盒、Cron / 心跳 +- 记忆、人格、书桌(工作区)、小安的不同状态 +- 技能、能力扩展、沙盒、主动提醒与日常巡检 - Electron + React 桌面端 ## 开发 @@ -56,7 +44,7 @@ npm run typecheck - 品牌:小安 / Xiaoan(`xiaoan-console` 主题,樱花粉控制台 UI) - 排版:`desktop/src/design/xiaoan-layout.css` — 见 [docs/XIAOAN-LAYOUT.md](./docs/XIAOAN-LAYOUT.md) - 插件:`plugins/xiaoan-core/`(think / brief / ask / plan / confirm) -- Agent 模板:新建助手时可选「小安」 +- 状态模板:新建助手时可选「小安」 - 本地开发上下文包:[docs/XIAOAN-LOCAL-DEV-CONTEXT.md](./docs/XIAOAN-LOCAL-DEV-CONTEXT.md) ## 文档 @@ -70,7 +58,7 @@ npm run typecheck | 维护者 | [docs/MAINTAINER.md](./docs/MAINTAINER.md) · [docs/FORK-CONFIG.md](./docs/FORK-CONFIG.md) | | 代码地图 | [docs/CODEMAP.md](./docs/CODEMAP.md) | | 控制台排版 | [docs/XIAOAN-LAYOUT.md](./docs/XIAOAN-LAYOUT.md) | -| Agent 方法论 | [docs/agent-design/XIAOAN-METHODOLOGY.md](./docs/agent-design/XIAOAN-METHODOLOGY.md) | +| 小安方法论 | [docs/agent-design/XIAOAN-METHODOLOGY.md](./docs/agent-design/XIAOAN-METHODOLOGY.md) | | Cursor 上下文 | [docs/XIAOAN-LOCAL-DEV-CONTEXT.md](./docs/XIAOAN-LOCAL-DEV-CONTEXT.md) | ## 路线图 @@ -79,4 +67,8 @@ npm run typecheck ## 许可证 -应用主体遵循上游 **Apache License 2.0**。定制层与文档另附 MIT 说明见 `UPSTREAM-NOTICE.md`。 +应用主体遵循 **Apache License 2.0**。定制层与文档说明见 `UPSTREAM-NOTICE.md`。 + +## 技术来源与开源参考 + +小安站在成熟开源项目之上做产品化内化:桌面壳与基础能力参考 [OpenHanako](https://github.com/liliMozi/openhanako),运行时底座为 Pi coding-agent。小安自己的默认体验、品牌、文案、配置、主题、工作区和方法论层在本仓库维护。详见 [UPSTREAM-NOTICE.md](./UPSTREAM-NOTICE.md) 与 [docs/FORK-CONFIG.md](./docs/FORK-CONFIG.md)。 diff --git a/README_EN.md b/README_EN.md index 63f071c48..0c3eeb194 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,160 +1,97 @@

- HanaAgent Banner + Xiaoan Banner

-

- HanaAgent -

- -

HanaAgent

+

Xiaoan

-

A personal AI agent with memory and soul

+

A desktop AI assistant that opens as Xiaoan: chat, memory, workspace, capabilities, and daily workflows ready by default.

中文版

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-lightgrey.svg)](https://github.com/liliMozi/openhanako/releases) +[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-lightgrey.svg)](https://github.com/An-an-525/An-Agent/releases) --- -## What is HanaAgent - -HanaAgent is a personal AI agent that is easier to use than traditional coding agents. It has memory, personality, and can act autonomously. Multiple agents can work together on your machine. - -As an assistant, it is gentle: no complex configuration files, no obscure jargon. HanaAgent is designed not just for coders, but for everyone who works at a computer. -As a tool, it is powerful: it remembers everything you've said, operates your computer, browses the web, searches for information, reads and writes files, executes code, manages schedules, and can even learn new skills on its own. - -## Features - -**Memory** — A custom memory system that keeps recent events sharp and lets older ones fade naturally. - -**Personality** — Not a generic "AI assistant". Each agent has its own voice and behavior through personality templates. Agents are self-contained folders, easy to back up and manage. - -**Tools** — Read/write files, run one-shot commands or persistent terminal sessions, browse the web, search the internet through browser-backed or API providers, take screenshots and segmented long screenshots, preview media, and inspect pages. Covers the vast majority of daily work scenarios. A server-first CLI can also attach to the same HanaAgent Server to show status, list sessions, and continue chats from a terminal. - -**Skills** — Built-in compatibility with the community Skills ecosystem. Agents can also install skills from GitHub or write their own. Strict safety review enabled by default. +## What is Xiaoan -**Character Cards & Skill Bundles** — Export and import agents as local-first character-card zip packages with allowlisted identity, avatar, optional memory, and skills. Skill Bundles are separate skill-pack infrastructure: group skills, drag them between bundles, toggle a whole bundle for an agent, and export a bundle as a standalone zip for migration or sharing. +**Xiaoan** (小安) is a desktop AI assistant: chat on the left, context and workspace on the right, with memory, tools, capabilities, and desk workflows built in. Users should open the app and see Xiaoan, not a generic framework that needs setup first. -**Multi-Agent** — Create multiple agents, each with independent memory, personality, and scheduled tasks. Agents can collaborate via channel group chats or delegate tasks to each other. +Default assistant identity: **Xiaoan** (小安). Branding and distribution metadata live in `shared/xiaoan-fork.json`. -**Desk** — Each agent has a desk for files and notes (Jian). Supports drag-and-drop, file preview, and workspace file-tree change watching, serving as an async collaboration space between you and your agent. +> Not a slide-deck tool. Xiaoan is a chat + workspace copilot: conversation, tools, memory, and a desk for files and notes. -**Full-Screen Media Viewer** — Click any image, SVG, or video from chat or the desk to open a dark-overlay viewer with wheel-zoom, drag-to-pan, `+` / `−` / `0` shortcuts, and left/right navigation between sibling media in the same session or folder. +## Maintainer model -**Session Management** — The sidebar can search chat history, prioritizing title matches and then searching message content. Old sessions can be archived, restored, or permanently deleted from settings. Selecting text in a chat message turns it into a composer quote card so follow-up questions keep the original context. +| Item | Value | +|------|--------| +| **Canonical repo** | https://github.com/An-an-525/An-Agent | +| **Branch** | `main` | +| **Config source of truth** | `shared/xiaoan-fork.json` | +| **Runtime home** | Production `~/.xiaoan` (legacy `~/.hanako` dual-read) · dev `XIAOAN_HOME=~/.xiaoan-dev` | -**Cron & Heartbeat** — Agents can run scheduled tasks and periodically check for file changes on the desk. The current automation executor separates "when to run" from "what to do": complex tasks still run as background Agent sessions, lightweight reminders can send direct notifications, and plugin actions can be scheduled too. +See [docs/FORK-CONFIG.md](./docs/FORK-CONFIG.md) and [UPSTREAM-NOTICE.md](./UPSTREAM-NOTICE.md). -**Sandbox** — Two-layer isolation: application-level PathGuard with four access tiers + OS-level sandboxing (macOS Seatbelt / Linux Bubblewrap / Windows restricted token). Agents can read ordinary system files, while writes and deletes stay limited to the workspace and managed data folders. On Windows, the command sandbox is a write-isolation model: reads use the current user's normal permissions, and network access keeps the current user's network permissions. macOS and Linux continue to use the network behavior provided by their platform sandbox backends. External network access can use system proxy, manual proxy, or direct mode. - -**Plugins** — Extensible plugin system with a convention-first architecture. Install community plugins by drag-and-drop. Plugins can contribute tools, skills, commands, agent templates, HTTP routes, Pi SDK extensions, LLM providers, pages, widgets, configuration schemas, and background tasks. Routes have direct access to core services (PluginContext injection) and can interact with agent sessions via the Session Bus; plugin cards flow through the same message-block and history replay pipeline as built-in cards. The two-level permission model (restricted / full-access) keeps advanced surfaces safe: `extensions/`, routes, providers, pages, and lifecycle hooks only load for full-access plugins. - -**Multi-Platform Bridge** — A single agent can connect to Telegram, Feishu, QQ, and WeChat bots simultaneously. Chat from any platform and remotely operate your computer. Bridge sessions carry platform context, and notifications can be delivered back to the current external platform. - -**Mobile & LAN Frontends** — HanaAgent Server can host the `/mobile/` PWA. Phones can sign in with a device access key or local account, view sessions, chat, and manage workbench files. Another desktop frontend can also connect to an existing LAN HanaAgent Server with a LAN URL and access key. +## Features -**i18n** — Interface available in 5 languages: Chinese, English, Japanese, Korean, and Traditional Chinese. +- Memory, personality, desk workspace, Xiaoan modes +- Capabilities, sandbox, reminders / daily checks +- Electron + React desktop shell +- **Xiaoan layer**: `plugins/xiaoan-core/` (think / brief / ask / plan / confirm), cherry-blossom console theme -## Screenshots - -

- HanaAgent Main Interface -

- -## Quick Start +## Quick start ### Download -**macOS (Apple Silicon / Intel):** download the latest `.dmg` from [Releases](https://github.com/liliMozi/openhanako/releases). - -The app is signed and notarized with an Apple Developer ID. macOS should allow it to launch directly. - -**Windows:** download the latest `.exe` installer from [Releases](https://github.com/liliMozi/openhanako/releases). - -> **Windows SmartScreen notice:** The installer is not yet code-signed. Windows Defender SmartScreen may show a warning on first run. Click **More info** → **Run anyway**. This is expected for unsigned builds. +Get the latest build from **[An-Agent Releases](https://github.com/An-an-525/An-Agent/releases)**: -**Linux:** download the latest `.AppImage` or `.deb` from [Releases](https://github.com/liliMozi/openhanako/releases). +| Platform | Artifact | +|----------|----------| +| macOS | `Xiaoan-*-macOS-*.dmg` / `.zip` | +| Windows | `Xiaoan-*-Windows-x64.exe` | +| Linux | `Xiaoan-*-Linux-*.AppImage` / `.deb` | -### First Run +Auto-update is **off by default** for Xiaoan builds. See [docs/XIAOAN-DISTRIBUTION.md](./docs/XIAOAN-DISTRIBUTION.md) before enabling it. -On first launch, an onboarding wizard will guide you through setup: choose a language, enter your name, connect a model provider (API key + base URL), and select three models — a **chat model** (main conversation), a **utility model** (lightweight tasks), and a **utility large model** (memory compilation and deep analysis). In settings you can also choose a **vision model** that lets text-only chat models work with image attachments through Vision Bridge. HanaAgent supports OpenAI-compatible providers, Anthropic-style providers, OAuth providers, and local models via Ollama. +### Development -## Architecture - -``` -core/ Engine orchestration + Managers (including PluginManager) -lib/ Core libraries (memory, tools, sandbox, bridge adapters) -server/ Hono HTTP + WebSocket server (standalone Node.js process) -hub/ Scheduler, ChannelRouter, EventBus -desktop/ Electron app + React frontend -shared/ Cross-layer utilities (config schema, error bus, model refs) -plugins/ Built-in system plugins (bundled into app) -skills2set/ Built-in skill definitions -scripts/ Build tools (server bundler, launcher, signing) -tests/ Vitest test suite -``` - -The engine layer coordinates multiple managers (Agent, Session, Model, Preferences, Skill, Channel, BridgeSession, Plugin, etc.) and exposes them through a unified facade. The Hub handles background tasks (heartbeat, automation / cron, channel routing, agent messaging, DM routing) independently of the active chat session. - -User-visible files inside a session are registered through `SessionFile` sidecars. Desktop, Bridge, Mobile PWA, and other remote frontends consume the same file identity according to their own capabilities. Bridge media delivery rules live in `.docs/BRIDGE-MEDIA-CAPABILITIES.md`; plugin file contribution rules live in `PLUGINS.md`. - -Local staged files are uploaded directly by platform adapters when possible: Telegram / Feishu / WeChat use their native upload flows, and QQ uses the official bot chunked-upload flow before sending `msg_type: 7` rich media. `preferences.bridge.mediaPublicBaseUrl` / `HANA_BRIDGE_PUBLIC_BASE_URL` are only for consumers or fallback paths that still require an internet-reachable URL. - -The server runs as a standalone Node.js process (spawned by Electron or independently), bundled via Vite with @vercel/nft for dependency tracing. It communicates with the Electron renderer through WebSocket. -User data is rooted at `HANA_HOME` (`~/.hanako` in production, `~/.hanako-dev` in development). The Pi SDK's own data is isolated under `${HANA_HOME}/.pi/`. - -## Tech Stack - -| Layer | Technology | -|-------|-----------| -| Desktop | Electron 42 | -| Frontend | React 19 + Zustand 5 + CSS Modules | -| Build | Vite 7 | -| Server | Hono + @hono/node-server | -| Agent Runtime | [Pi SDK](https://github.com/badlogic/pi-mono) | -| Database | better-sqlite3 (WAL mode) | -| Testing | Vitest | -| i18n | 5 languages (zh / en / ja / ko / zh-TW) | - -## Platform Support - -| Platform | Status | -|----------|--------| -| macOS (Apple Silicon) | Supported (signed & notarized) | -| macOS (Intel) | Supported | -| Windows | Beta | -| Linux | Supported (AppImage / deb) | -| Mobile (PWA) | v0: phone sessions and workbench access through the same HanaAgent Server | - -## Development +Requires **Node.js 24.x**. ```bash -# Install dependencies npm install +npm start # Electron desktop +npm run start:vite # dev mode (HMR) +npm test +npm run typecheck +``` -# Start with Electron (builds renderer first) -npm start +## Architecture (high level) -# Start with Vite HMR (run npm run dev:renderer first) -npm run start:vite +``` +core/ Engine + managers +lib/ Memory, tools, sandbox, bridge +server/ Hono HTTP + WebSocket (standalone Node process) +desktop/ Electron + React UI +shared/ Fork config, runtime contract, cross-layer utils +plugins/ Built-in system plugins (incl. xiaoan-core) +``` -# Server only -npm run server +Read [docs/CODEMAP.md](./docs/CODEMAP.md) for navigation. -# Server-first CLI -npm run cli +## Documentation -# Run tests -npm test +Start at [docs/README.md](./docs/README.md). -# Type check -npm run typecheck -``` +| Topic | Doc | +|-------|-----| +| Local dev | [docs/LOCAL-DEV.md](./docs/LOCAL-DEV.md) | +| Distribution / updates | [docs/XIAOAN-DISTRIBUTION.md](./docs/XIAOAN-DISTRIBUTION.md) | +| Internalization | [docs/XIAOAN-CODEBASE-INTERNALIZATION.md](./docs/XIAOAN-CODEBASE-INTERNALIZATION.md) | +| Methodology | [docs/agent-design/XIAOAN-METHODOLOGY.md](./docs/agent-design/XIAOAN-METHODOLOGY.md) | -## Acknowledgments +## Open-source references -- [tw93/kami](https://github.com/tw93/kami): the progressive-disclosure structure of the beautify plugin's HTML aesthetic guide (a router entry with flat on-demand sections) was inspired by this project. +Xiaoan productizes and internalizes mature open-source foundations. The desktop shell and baseline capabilities reference [OpenHanako](https://github.com/liliMozi/openhanako), and the runtime base is Pi coding-agent. Xiaoan's default experience, brand, copy, configuration, theme, workspace, and methodology layer are maintained in this repository. ## License @@ -162,11 +99,7 @@ npm run typecheck ## Links -> The repository and release URLs intentionally remain under the legacy `openhanako` path during the current migration. The repository rename is a separate later step. - -- [Homepage](https://openhanako.com) -- [Report an Issue](https://github.com/liliMozi/openhanako/issues) -- [Security](https://github.com/liliMozi/openhanako/security) -- [Security Policy](SECURITY.md) -- [Plugin Development](PLUGINS.md) +- [Releases](https://github.com/An-an-525/An-Agent/releases) +- [Report an Issue](https://github.com/An-an-525/An-Agent/issues) - [Contributing](CONTRIBUTING.md) +- [Plugin development](PLUGINS_EN.md) diff --git a/SECURITY.md b/SECURITY.md index a2a5f3903..a57c13753 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,15 +1,15 @@ # Security Policy -This project is now marketed as HanaAgent. During the rename transition, the repository and security-reporting URLs intentionally remain on the legacy `openhanako` GitHub path. +This project is Xiaoan, a desktop AI assistant maintained at `An-an-525/An-Agent`. ## Reporting a Vulnerability If you discover a security vulnerability, please report it through GitHub: - Open a private vulnerability report in the repository Security tab when available: - https://github.com/liliMozi/openhanako/security + https://github.com/An-an-525/An-Agent/security - If private reporting is not available, open an issue: - https://github.com/liliMozi/openhanako/issues + https://github.com/An-an-525/An-Agent/issues Please include: - Description of the vulnerability diff --git a/build/installer.nsh b/build/installer.nsh index f62b70d91..c4ce1a390 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -1,7 +1,7 @@ -; installer.nsh - NSIS custom hooks for HanaAgent installer +; installer.nsh - NSIS custom hooks for Xiaoan installer ; -; Owns the Windows overlay boundary for HanaAgent installs. The installer may -; replace HanaAgent-owned program files, while user/runtime state stays outside +; Owns the Windows overlay boundary for Xiaoan installs. The installer may +; replace Xiaoan-owned program files, while user/runtime state stays outside ; $INSTDIR. ; Disable CRC integrity check. electron-builder's post-compilation PE editing @@ -37,10 +37,16 @@ CRCCheck off !macroend !macro hanakoFindRunningProcesses _RETURN - !insertmacro hanakoFindProcess HanaAgent.exe ${_RETURN} + !insertmacro hanakoFindProcess Xiaoan.exe ${_RETURN} + ${If} ${_RETURN} != 0 + !insertmacro hanakoFindProcess HanaAgent.exe ${_RETURN} + ${EndIf} ${If} ${_RETURN} != 0 !insertmacro hanakoFindProcess Hanako.exe ${_RETURN} ${EndIf} + ${If} ${_RETURN} != 0 + !insertmacro hanakoFindProcess xiaoan-server.exe ${_RETURN} + ${EndIf} ${If} ${_RETURN} != 0 !insertmacro hanakoFindProcess hana-server.exe ${_RETURN} ${EndIf} @@ -61,8 +67,10 @@ CRCCheck off !macroend !macro hanakoKillRunningProcesses _FORCE + !insertmacro hanakoKillProcess Xiaoan.exe ${_FORCE} !insertmacro hanakoKillProcess HanaAgent.exe ${_FORCE} !insertmacro hanakoKillProcess Hanako.exe ${_FORCE} + !insertmacro hanakoKillProcess xiaoan-server.exe ${_FORCE} !insertmacro hanakoKillProcess hana-server.exe ${_FORCE} !macroend @@ -76,10 +84,10 @@ CRCCheck off Push $0 Push $R2 StrCpy $R2 "" - !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\${APP_EXECUTABLE_FILENAME}" "HanaAgent.exe" + !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\${APP_EXECUTABLE_FILENAME}" "Xiaoan.exe" !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\resources\app.asar" "resources\app.asar" !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\resources\app-update.yml" "resources\app-update.yml" - !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\resources\server\hana-server.exe" "resources\server\hana-server.exe" + !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\resources\server\xiaoan-server.exe" "resources\server\xiaoan-server.exe" !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\resources\server\bootstrap.js" "resources\server\bootstrap.js" !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\resources\server\bundle\index.js" "resources\server\bundle\index.js" !insertmacro hanakoRequireInstallSurfaceFile "$INSTDIR\resources\server\node_modules\better-sqlite3\build\Release\better_sqlite3.node" "better-sqlite3 native addon" @@ -89,13 +97,13 @@ CRCCheck off StrCpy $R2 "$R2$\r$\n- PortableGit bash.exe: $INSTDIR\resources\git\bin\bash.exe or $INSTDIR\resources\git\usr\bin\bash.exe" ${If} $R2 != "" - DetailPrint "HanaAgent install surface self-check failed." + DetailPrint "Xiaoan install surface self-check failed." FileOpen $0 "$INSTDIR\hanaagent-install-diagnostics.log" w - FileWrite $0 "HanaAgent install surface self-check failed.$\r$\n" + FileWrite $0 "Xiaoan install surface self-check failed.$\r$\n" FileWrite $0 "Install dir: $INSTDIR$\r$\n" FileWrite $0 "Missing or unreadable files:$R2$\r$\n" FileClose $0 - MessageBox MB_OK|MB_ICONSTOP "HanaAgent installation is incomplete. Missing or unreadable files:$R2$\r$\n$\r$\nDiagnostic file:$\r$\n$INSTDIR\hanaagent-install-diagnostics.log" + MessageBox MB_OK|MB_ICONSTOP "Xiaoan installation is incomplete. Missing or unreadable files:$R2$\r$\n$\r$\nDiagnostic file:$\r$\n$INSTDIR\hanaagent-install-diagnostics.log" SetErrorLevel 1 !insertmacro hanakoInstallTimingMark "installSurfaceSelfCheck" "failed" !insertmacro hanakoPersistInstallTiming @@ -105,7 +113,7 @@ CRCCheck off ${Else} Delete "$INSTDIR\hanaagent-install-diagnostics.log" Delete "$INSTDIR\hanako-install-diagnostics.log" - DetailPrint "HanaAgent install surface self-check passed." + DetailPrint "Xiaoan install surface self-check passed." ${EndIf} Pop $R2 Pop $0 @@ -171,7 +179,7 @@ CRCCheck off FileWrite $0 ` $$_.ProcessId -ne $$selfPid -and $$_.ProcessId -ne $$installerPid -and ((Test-HanaPath $$_.ExecutablePath) -or (Test-HanaCommand $$_.CommandLine))$\r$\n` FileWrite $0 `})$\r$\n` FileWrite $0 `$$matches | ForEach-Object {$\r$\n` - FileWrite $0 ` Write-Output ("HanaAgent-owned process still running: {0} pid={1} path={2}" -f $$_.Name, $$_.ProcessId, $$_.ExecutablePath)$\r$\n` + FileWrite $0 ` Write-Output ("Xiaoan-owned process still running: {0} pid={1} path={2}" -f $$_.Name, $$_.ProcessId, $$_.ExecutablePath)$\r$\n` FileWrite $0 `}$\r$\n` FileWrite $0 `if ($$matches.Count -gt 0) { exit 0 } else { exit 1 }$\r$\n` FileClose $0 @@ -212,7 +220,7 @@ CRCCheck off !macro hanakoBypassOldUninstallerForUpdate ${If} ${isUpdated} - DetailPrint "Update mode detected; bypassing the previous uninstaller and preparing a HanaAgent-owned overlay." + DetailPrint "Update mode detected; preparing Xiaoan's managed install files." !insertmacro hanakoPrepareOwnedOverlay DeleteRegKey SHELL_CONTEXT "${UNINSTALL_REGISTRY_KEY}" !ifdef UNINSTALL_REGISTRY_KEY_2 @@ -272,7 +280,7 @@ CRCCheck off !insertmacro hanakoStopInstallDirProcesses !insertmacro hanakoFindInstallDirProcesses $R0 ${If} $R0 == 0 - DetailPrint "Detected HanaAgent-owned process in install directory; closing it before install." + DetailPrint "Detected a Xiaoan process in the install directory; closing it before install." Sleep 500 !insertmacro hanakoStopInstallDirProcesses @@ -281,9 +289,9 @@ CRCCheck off !insertmacro hanakoFindInstallDirProcesses $R0 ${If} $R0 == 0 IntOp $R1 $R1 + 1 - DetailPrint "Waiting for HanaAgent-owned install-directory processes to close." + DetailPrint "Waiting for Xiaoan install-directory processes to close." ${If} $R1 > 2 - DetailPrint "HanaAgent-owned install-directory processes still running; asking user to retry." + DetailPrint "Xiaoan install-directory processes are still running; asking user to retry." MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY hanako_retry_install_dir_close Quit hanako_retry_install_dir_close: @@ -298,7 +306,7 @@ CRCCheck off ${IfNot} ${isUpdated} !insertmacro hanakoFindRunningProcesses $R0 ${If} $R0 == 0 - DetailPrint "Detected HanaAgent.exe, Hanako.exe, or hana-server.exe; closing them before install." + DetailPrint "Detected Xiaoan or a legacy Xiaoan process; closing it before install." !insertmacro hanakoKillRunningProcesses 0 Sleep 500 @@ -313,9 +321,9 @@ CRCCheck off !insertmacro hanakoFindRunningProcesses $R0 ${If} $R0 == 0 IntOp $R1 $R1 + 1 - DetailPrint "Waiting for HanaAgent.exe, Hanako.exe, or hana-server.exe to close." + DetailPrint "Waiting for Xiaoan and legacy Xiaoan processes to close." ${If} $R1 > 2 - DetailPrint "HanaAgent.exe, Hanako.exe, or hana-server.exe still running; asking user to retry." + DetailPrint "Xiaoan or a legacy Xiaoan process is still running; asking user to retry." MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY hanako_retry_close Quit hanako_retry_close: @@ -396,7 +404,7 @@ CRCCheck off !macro hanakoRemoveOwnedInstallTrees !insertmacro hanakoInstallTimingMark "removeOwnedInstallTrees" "start" - DetailPrint "Removing HanaAgent-owned install files" + DetailPrint "Removing Xiaoan managed install files" SetOutPath "$TEMP" RMDir /r "$INSTDIR\resources\server" RMDir /r "$INSTDIR\resources\git" @@ -446,9 +454,9 @@ CRCCheck off !macro customUnInstallCheck !insertmacro hanakoInstallTimingMark "customUnInstallCheck" "start" ${If} ${Errors} - DetailPrint `Previous uninstaller could not be launched; preparing a HanaAgent-owned overlay.` + DetailPrint `Previous uninstaller could not be launched; preparing Xiaoan managed install files.` ${ElseIf} $R0 != 0 - DetailPrint `Previous uninstaller exited with code $R0; preparing a HanaAgent-owned overlay.` + DetailPrint `Previous uninstaller exited with code $R0; preparing Xiaoan managed install files.` ${EndIf} !insertmacro hanakoPrepareOwnedOverlay ClearErrors @@ -458,9 +466,9 @@ CRCCheck off !macro customUnInstallCheckCurrentUser !insertmacro hanakoInstallTimingMark "customUnInstallCheckCurrentUser" "start" ${If} ${Errors} - DetailPrint `Previous current-user uninstaller could not be launched; continuing with HanaAgent-owned overlay.` + DetailPrint `Previous current-user uninstaller could not be launched; continuing with Xiaoan managed install files.` ${ElseIf} $R0 != 0 - DetailPrint `Previous current-user uninstaller exited with code $R0; continuing with HanaAgent-owned overlay.` + DetailPrint `Previous current-user uninstaller exited with code $R0; continuing with Xiaoan managed install files.` ${EndIf} !insertmacro hanakoPrepareOwnedOverlay ClearErrors diff --git a/cli/args.ts b/cli/args.ts index 0ec5c436d..b01633096 100644 --- a/cli/args.ts +++ b/cli/args.ts @@ -1,3 +1,5 @@ +import { getProductNameEn } from "../shared/xiaoan-fork.ts"; + const COMMANDS = new Set(["serve", "status", "sessions", "continue", "chat", "help"]); export function parseCliArgs(argv = []) { @@ -49,17 +51,17 @@ function requireValue(args, index, flag) { } export function helpText() { - return `Hana CLI + return `${getProductNameEn()} CLI Usage: - hana serve [-- server args] Start a headless HanaAgent Server + hana serve [-- server args] Start a headless ${getProductNameEn()} Server hana status Show local server and agent status hana sessions List recent sessions hana continue [index|path] Continue a recent session hana chat [--plain] Open chat Connection options: - --url Connect to a specific HanaAgent Server + --url Connect to a specific ${getProductNameEn()} Server --token Bearer token for that server --session Chat in a specific session `; diff --git a/cli/chat.ts b/cli/chat.ts index de96067e0..78c62b5b6 100644 --- a/cli/chat.ts +++ b/cli/chat.ts @@ -1,4 +1,5 @@ import readline from "readline"; +import { getProductNameEn } from "../shared/xiaoan-fork.ts"; import { createTerminalTheme, ansi, paint } from "./terminal-theme.ts"; export async function printStatus(client, connection) { @@ -7,7 +8,7 @@ export async function printStatus(client, connection) { client.identity().catch(() => null), ]); const theme = createTerminalTheme(health.agentYuan); - console.log(`${paint(theme, theme.symbol)} HanaAgent Server`); + console.log(`${paint(theme, theme.symbol)} ${getProductNameEn()} Server`); console.log(` ${ansi.dim}URL${ansi.reset} ${connection.baseUrl}`); console.log(` ${ansi.dim}Version${ansi.reset} ${identity?.version || health.version || "unknown"}`); console.log(` ${ansi.dim}Studio${ansi.reset} ${identity?.studioLabel || identity?.studioId || "local"}`); @@ -288,8 +289,8 @@ async function loadContext(client) { || null; return { agentId: health.agentId || current?.id || null, - agentName: health.agent || current?.name || "Hana", - agentYuan: health.agentYuan || current?.yuan || "hanako", + agentName: health.agent || current?.name || "Xiaoan", + agentYuan: health.agentYuan || current?.yuan || "xiaoan", userName: health.user || "you", }; } diff --git a/cli/entry.ts b/cli/entry.ts index 4ab5b53fa..fcb93a0ff 100644 --- a/cli/entry.ts +++ b/cli/entry.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import path from "path"; +import { getProductNameEn } from "../shared/xiaoan-fork.ts"; import { fileURLToPath } from "url"; import { parseCliArgs, helpText } from "./args.ts"; import { resolveConnection } from "./local-server.ts"; @@ -35,7 +36,7 @@ export async function main(argv = process.argv.slice(2)) { let connection: any = resolveConnection({ url: args.url, token: args.token }); if (!connection.ok && shouldAutoStartServer(args)) { - console.error(`${ansi.dim}Starting local HanaAgent Server...${ansi.reset}`); + console.error(`${ansi.dim}Starting local ${getProductNameEn()} Server...${ansi.reset}`); connection = await startLocalServerAndWait({ projectRoot: PROJECT_ROOT }); } if (!connection.ok) { diff --git a/cli/local-server.ts b/cli/local-server.ts index 4d05293bc..dcb5235d6 100644 --- a/cli/local-server.ts +++ b/cli/local-server.ts @@ -1,14 +1,15 @@ import fs from "fs"; import os from "os"; import path from "path"; +import { getProductNameEn } from "../shared/xiaoan-fork.ts"; export function resolveCliHanaHome(env = process.env) { - return resolveHomePath(env.HANA_HOME || path.join(os.homedir(), ".hanako")); + return resolveHomePath(env.XIAOAN_HOME || env.HANA_HOME || path.join(os.homedir(), ".xiaoan")); } function resolveHomePath(value) { const raw = String(value || "").trim(); - if (!raw) return path.join(os.homedir(), ".hanako"); + if (!raw) return path.join(os.homedir(), ".xiaoan"); if (raw === "~") return os.homedir(); if (raw.startsWith(`~${path.sep}`) || raw.startsWith("~/")) { return path.resolve(path.join(os.homedir(), raw.slice(2))); @@ -23,7 +24,7 @@ export function readLocalServerInfo({ hanaHome = resolveCliHanaHome(), checkProc ok: false, reason: "missing_server_info", filePath, - message: `No running HanaAgent Server was found at ${filePath}`, + message: `No running ${getProductNameEn()} Server was found at ${filePath}`, }; } @@ -53,7 +54,7 @@ export function readLocalServerInfo({ hanaHome = resolveCliHanaHome(), checkProc ok: false, reason: "stale_server_info", filePath, - message: `HanaAgent Server process ${info.pid} is no longer running`, + message: `${getProductNameEn()} Server process ${info.pid} is no longer running`, }; } diff --git a/cli/server-runner.ts b/cli/server-runner.ts index b0e851af9..9e9a2277b 100644 --- a/cli/server-runner.ts +++ b/cli/server-runner.ts @@ -1,5 +1,6 @@ import fs from "fs"; import path from "path"; +import { getProductNameEn } from "../shared/xiaoan-fork.ts"; import { spawn } from "child_process"; import { readLocalServerInfo, resolveCliHanaHome } from "./local-server.ts"; @@ -75,7 +76,7 @@ export async function startLocalServerAndWait({ await delay(intervalMs); } - throw new Error(`HanaAgent Server did not become ready within ${Math.round(timeoutMs / 1000)}s`); + throw new Error(`${getProductNameEn()} Server did not become ready within ${Math.round(timeoutMs / 1000)}s`); } function delay(ms: number) { diff --git a/core/agent.ts b/core/agent.ts index 100518f52..61b7406f8 100644 --- a/core/agent.ts +++ b/core/agent.ts @@ -344,13 +344,17 @@ export class Agent { log(` [agent] 4. FactStore + SummaryManager 完成`); - // utility 模型:用户未配置时 fallback 到聊天模型 + // 保存解析函数:每次 tick 现场调用,拿到最新凭证。 + // 不缓存解析结果——provider key/url/api 变更后 tick 自动恢复,无需重启 agent。 + this._resolveModel = resolveModel || null; + + // utility 模型:用户未配置时 fallback 到聊天模型;旧偏好指向不可用模型时运行时回退。 const chatModelRef = this._config.models?.chat || null; const userSetUtility = sharedModels.utility || this._config.models?.utility || null; const userSetUtilityLarge = sharedModels.utility_large || this._config.models?.utility_large || null; - this._utilityModel = userSetUtility || chatModelRef; - this._memoryModel = userSetUtilityLarge || chatModelRef; + this._utilityModel = this._resolveUsableModelRef(userSetUtility, chatModelRef, "utility"); + this._memoryModel = this._resolveUsableModelRef(userSetUtilityLarge, chatModelRef, "utility_large"); if (!userSetUtility && chatModelRef) { moduleLog.log(`utility 模型未配置,使用聊天模型作为工具模型`); @@ -359,10 +363,6 @@ export class Agent { moduleLog.log(`utility_large 模型未配置,使用聊天模型作为记忆模型`); } - // 保存解析函数:每次 tick 现场调用,拿到最新凭证。 - // 不缓存解析结果——provider key/url/api 变更后 tick 自动恢复,无需重启 agent。 - this._resolveModel = resolveModel || null; - // 启动时试探性 resolve 一次,只为打一条启动告警(运行时由 ticker 各调用点的 try/catch 处理) if (this._memoryModel && this._resolveModel) { try { @@ -719,6 +719,28 @@ export class Agent { setUtilityModel(val) { this._utilityModel = val; } setMemoryModel(val) { this._memoryModel = val; } + _resolveUsableModelRef(preferredRef, fallbackRef, role) { + const ref = preferredRef || fallbackRef; + if (!ref || !this._resolveModel) return ref; + + try { + this._resolveModel(ref, this._config); + return ref; + } catch (err) { + if (preferredRef && fallbackRef && JSON.stringify(preferredRef) !== JSON.stringify(fallbackRef)) { + try { + this._resolveModel(fallbackRef, this._config); + moduleLog.warn(`${role} 模型不可用,已临时回退到聊天模型 — ${err.message}`); + this._cb?.emitDevLog?.(`${role} 模型不可用,已临时回退到聊天模型`, "warn"); + return fallbackRef; + } catch { + return ref; + } + } + return ref; + } + } + /** * 为某个会话面创建带作用域的 search_memory 实例(同一 FactStore,不复制数据归属)。 * 频道 phone 会话用它替换默认实例:默认排除其它频道的事实,跨频道需显式参数(#1670)。 diff --git a/core/engine.ts b/core/engine.ts index fb11cd707..900727c02 100644 --- a/core/engine.ts +++ b/core/engine.ts @@ -1437,6 +1437,8 @@ export class HanaEngine { } getSidebarUiPrefs() { return this._prefs.getSidebarUiPrefs(); } setSidebarUiPrefs(partial) { return this._prefs.setSidebarUiPrefs(partial); } + getSettingsUiPrefs() { return this._prefs.getSettingsUiPrefs(); } + setSettingsUiPrefs(partial) { return this._prefs.setSettingsUiPrefs(partial); } getPluginUiPrefs() { return this._prefs.getPluginUiPrefs(); } setPluginUiPrefs(partial) { return this._prefs.setPluginUiPrefs(partial); } getPluginDevToolsEnabled() { return this._prefs.getPluginDevToolsEnabled(); } diff --git a/core/first-run.ts b/core/first-run.ts index 6e388be0f..0f7ffdd64 100644 --- a/core/first-run.ts +++ b/core/first-run.ts @@ -22,6 +22,11 @@ import { XIAOAN_DEFAULT_YUAN, XIAOAN_LEGACY_AGENT_ID, } from "../shared/xiaoan-defaults.ts"; +import { + XIAOAN_BUILTIN_RELAY_PROVIDER_ID, + getBuiltinRelayProviderDefaults, +} from "../shared/xiaoan-builtin-relay.ts"; +import { getProvidersYamlHeaderZh } from "../shared/xiaoan-fork.ts"; import { migrateXiaoanForkDefaults } from "./xiaoan-fork-migrate.ts"; import { migrateXiaoanRuntime } from "./xiaoan-runtime-migrate.ts"; import { createModuleLogger } from "../lib/debug-log.ts"; @@ -137,10 +142,40 @@ export function ensureFirstRun(xiaoanHome, productDir): FirstRunReport { // 6. 小安 fork:软迁移 upstream 默认名/元 + 启用 handbook skill migrateXiaoanForkDefaults(xiaoanHome); + seedBuiltinRelayProvider(xiaoanHome); return { invalidAgentDirs, repairedDefaultAgent, defaultConfigBackupPath }; } +function seedBuiltinRelayProvider(xiaoanHome: string): void { + const addedModelsPath = path.join(xiaoanHome, "added-models.yaml"); + let raw: any = {}; + if (fs.existsSync(addedModelsPath)) { + try { + raw = YAML.load(fs.readFileSync(addedModelsPath, "utf-8")) || {}; + } catch { + return; + } + } + + const providers = raw.providers && typeof raw.providers === "object" && !Array.isArray(raw.providers) + ? raw.providers + : {}; + if (providers[XIAOAN_BUILTIN_RELAY_PROVIDER_ID]) return; + + const defaults = getBuiltinRelayProviderDefaults(); + providers[XIAOAN_BUILTIN_RELAY_PROVIDER_ID] = { + base_url: defaults.base_url, + api: defaults.api, + }; + raw.providers = providers; + + const yaml = getProvidersYamlHeaderZh() + + "# 小安内置中转站默认入口。API Key 由用户在设置中填写;此处不写入任何密钥。\n\n" + + YAML.dump(raw, { indent: 2, lineWidth: -1, sortKeys: false, quotingType: '"' }); + fs.writeFileSync(addedModelsPath, yaml, "utf-8"); +} + type AgentDirClassification = | { status: "valid" } | { status: "invalid"; reason: "config_missing" | "config_unreadable"; detail?: string }; diff --git a/core/migrate-providers.ts b/core/migrate-providers.ts index 833e76d7b..3d5816354 100644 --- a/core/migrate-providers.ts +++ b/core/migrate-providers.ts @@ -14,6 +14,7 @@ import fs from "fs"; import path from "path"; +import { getProvidersYamlHeaderZh } from "../shared/xiaoan-fork.ts"; import YAML from "js-yaml"; import { safeReadYAMLSync } from "../shared/safe-fs.ts"; import { fromRoot } from "../shared/hana-root.ts"; @@ -89,7 +90,7 @@ export function migrateToProvidersYaml(hanakoHome: string, agentsDir: string, lo const data = existingRaw || {}; data._migrated = true; const header = - "# HanaAgent 供应商配置(全局,跨 agent 共享)\n" + + getProvidersYamlHeaderZh() + "# 由设置页面管理\n\n"; atomicWriteYAML(providersPath, data, header); log("[migrate-providers] 无旧数据需要迁移,已标记完成"); @@ -196,7 +197,7 @@ export function migrateToProvidersYaml(hanakoHome: string, agentsDir: string, lo raw.providers = providers; raw._migrated = true; const header = - "# HanaAgent 供应商配置(全局,跨 agent 共享)\n" + + getProvidersYamlHeaderZh() + "# 由设置页面管理\n\n"; atomicWriteYAML(providersPath, raw, header); log("[migrate-providers] added-models.yaml 已更新"); diff --git a/core/migrations.ts b/core/migrations.ts index 0ad456f74..bc2be6ac3 100644 --- a/core/migrations.ts +++ b/core/migrations.ts @@ -9,6 +9,7 @@ */ import fs from "fs"; import path from "path"; +import { getProvidersYamlHeaderZh } from "../shared/xiaoan-fork.ts"; import YAML from "js-yaml"; import { atomicWriteSync, safeReadYAMLSync } from "../shared/safe-fs.ts"; import { @@ -1069,7 +1070,7 @@ function migrateMiniMaxTokenPlanAnthropicEndpoint(ctx) { provider.api = MINIMAX_CURRENT_ANTHROPIC_API; const header = - "# HanaAgent 供应商配置(全局,跨 agent 共享)\n" + + getProvidersYamlHeaderZh() + "# 由设置页面管理\n\n"; const yamlStr = header + YAML.dump(raw, { indent: 2, @@ -1126,7 +1127,7 @@ function migrateVisionToImage(ctx) { } if (changed) { const header = - "# HanaAgent 供应商配置(全局,跨 agent 共享)\n" + + getProvidersYamlHeaderZh() + "# 由设置页面管理\n\n"; const yamlStr = header + YAML.dump(raw, { indent: 2, @@ -2108,7 +2109,7 @@ function migrateGeminiOpenAICompatToNative(ctx) { if (patched > 0) { const header = - "# HanaAgent 供应商配置(全局,跨 agent 共享)\n" + + getProvidersYamlHeaderZh() + "# 由设置页面管理\n\n"; const yamlStr = header + YAML.dump(raw, { indent: 2, @@ -2452,7 +2453,7 @@ function promoteAgentVideoOverrides(ctx) { if (addedModelsChanged) { const header = - "# HanaAgent 供应商配置(全局,跨 agent 共享)\n" + + getProvidersYamlHeaderZh() + "# 由设置页面管理\n\n"; const tmp = ymlPath + ".tmp"; fs.writeFileSync( @@ -2786,7 +2787,7 @@ function repairLegacyDeepSeekProviderModelIds(ctx) { if (patched > 0) { const header = - "# HanaAgent 供应商配置(全局,跨 agent 共享)\n" + + getProvidersYamlHeaderZh() + "# 由设置页面管理\n\n"; const tmp = ymlPath + ".tmp"; fs.writeFileSync( diff --git a/core/preferences-manager.ts b/core/preferences-manager.ts index edf5ec67b..bf3f21501 100644 --- a/core/preferences-manager.ts +++ b/core/preferences-manager.ts @@ -31,6 +31,10 @@ import { mergeSidebarUiPrefs, normalizeSidebarUiPrefs, } from "../shared/sidebar-ui-state.ts"; +import { + mergeSettingsUiPrefs, + normalizeSettingsUiPrefs, +} from "../shared/settings-ui-preferences.ts"; import { normalizeWorkspacePath } from "../shared/workspace-history.ts"; import { normalizeNetworkProxyConfig } from "../shared/network-proxy.ts"; import { @@ -45,8 +49,10 @@ import { mergeBrowserPreferences, normalizeBrowserPreferences, } from "../shared/browser-preferences.ts"; +import { XIAOAN_FORK } from "../shared/xiaoan-fork.ts"; import { createModuleLogger } from "../lib/debug-log.ts"; import { normalizeSessionThinkingLevel } from "./session-thinking-level.ts"; +import { DEFAULT_PLUGIN_UI_TAB_ORDER } from "../shared/builtin-plugins.ts"; const log = createModuleLogger("preferences"); @@ -538,6 +544,19 @@ export class PreferencesManager { return this.getSidebarUiPrefs(); } + /** 读取设置界面 UI 偏好(开发者模式等)。 */ + getSettingsUiPrefs() { + return normalizeSettingsUiPrefs(this._cache.settings_ui || {}); + } + + /** 合并写入设置界面 UI 偏好。 */ + setSettingsUiPrefs(partial) { + const prefs = this._mutableCopy(); + prefs.settings_ui = mergeSettingsUiPrefs(prefs.settings_ui || {}, partial || {}); + this.savePreferences(prefs); + return this.getSettingsUiPrefs(); + } + /** 读取时区偏好(全局) */ getTimezone() { return this._cache.timezone || ""; @@ -642,10 +661,11 @@ export class PreferencesManager { /** 读取插件 UI 偏好(hiddenWidgets / hiddenTabs / tabOrder) */ getPluginUiPrefs() { const raw = this._cache.plugin_ui; + const savedOrder = Array.isArray(raw?.tabOrder) ? raw.tabOrder : []; return { hiddenWidgets: Array.isArray(raw?.hiddenWidgets) ? raw.hiddenWidgets : [], hiddenTabs: Array.isArray(raw?.hiddenTabs) ? raw.hiddenTabs : [], - tabOrder: Array.isArray(raw?.tabOrder) ? raw.tabOrder : [], + tabOrder: savedOrder.length > 0 ? savedOrder : [...DEFAULT_PLUGIN_UI_TAB_ORDER], }; } @@ -761,9 +781,10 @@ export class PreferencesManager { this.savePreferences(prefs); } - /** 读取"自动检查更新"开关:默认 true */ + /** 读取"自动检查更新"开关:默认跟随小安 fork 配置 */ getAutoCheckUpdates() { - return this._cache.auto_check_updates !== false; + if (typeof this._cache.auto_check_updates === "boolean") return this._cache.auto_check_updates; + return XIAOAN_FORK.defaultAutoCheckUpdates === true; } /** 保存"自动检查更新"开关 */ diff --git a/core/provider-media-config.ts b/core/provider-media-config.ts index 6ac24539e..653135ec0 100644 --- a/core/provider-media-config.ts +++ b/core/provider-media-config.ts @@ -1,5 +1,5 @@ - import fs from "fs"; +import { getProvidersYamlHeaderZh } from "../shared/xiaoan-fork.ts"; import path from "path"; import YAML from "js-yaml"; import { atomicWriteSync, safeReadYAMLSync } from "../shared/safe-fs.ts"; @@ -112,7 +112,7 @@ export function migrateProviderMediaConfig(hanakoHome, log: (...args: any[]) => if (!changed) return false; const header = - "# HanaAgent 供应商配置(全局,跨 agent 共享)\n" + + getProvidersYamlHeaderZh() + "# 由设置页面管理\n\n"; const data = { ...existing, providers }; const yamlStr = header + YAML.dump(data, { diff --git a/core/provider-registry.ts b/core/provider-registry.ts index c2ef7c6f6..39fc6b861 100644 --- a/core/provider-registry.ts +++ b/core/provider-registry.ts @@ -11,6 +11,7 @@ */ import fs from "fs"; +import { getAgentConfigYamlHeaderZh } from "../shared/xiaoan-fork.ts"; import path from "path"; import YAML from "js-yaml"; import { atomicWriteSync, safeReadYAMLSync } from "../shared/safe-fs.ts"; @@ -315,7 +316,7 @@ function normalizeUserMediaModels(providerId, userConfig, capabilityName, declar return result; } -// ── 内置插件 ──────────────────────────────────────────────────────────────── +// ── 内置插件(含小安产品层 anxin-relay,见 shared/xiaoan-settings-product-notes.ts) ── import { dashscopePlugin } from "../lib/providers/dashscope.ts"; import { agnesPlugin } from "../lib/providers/agnes.ts"; @@ -356,8 +357,11 @@ import { dashscopeCodingPlugin } from "../lib/providers/dashscope-coding.ts"; import { kimiCodingPlugin } from "../lib/providers/kimi-coding.ts"; import { volcegineCodingPlugin } from "../lib/providers/volcengine-coding.ts"; import { zhipuCodingPlugin } from "../lib/providers/zhipu-coding.ts"; +// 小安内置中转(非开发者模式默认模型来源)· 见 shared/xiaoan-settings-product-notes.ts +import { anxinRelayPlugin } from "../lib/providers/anxin-relay.ts"; const BUILTIN_PLUGINS = [ + anxinRelayPlugin, agnesPlugin, dashscopePlugin, openaiPlugin, @@ -599,7 +603,7 @@ export class ProviderRegistry { if (Object.keys(overrides).length === 0) { delete cfg.models.overrides; } - const header = "# HanaAgent 助手配置\n# 由设置页面管理,手动编辑也可以\n\n"; + const header = getAgentConfigYamlHeaderZh(); const yamlStr = header + YAML.dump(cfg, { indent: 2, lineWidth: -1, sortKeys: false, quotingType: '"', forceQuotes: false }); atomicWriteSync(cfgPath, yamlStr); } diff --git a/core/server-identity.ts b/core/server-identity.ts index 4fa834b53..e50079fb2 100644 --- a/core/server-identity.ts +++ b/core/server-identity.ts @@ -195,7 +195,7 @@ function createLocalServerNodeIdentity({ now }) { execution: { kind: "local_process", }, - label: "Local Hana", + label: "Local Xiaoan", createdAt: now, updatedAt: now, }; diff --git a/core/session-coordinator.ts b/core/session-coordinator.ts index 266ad65de..c2466d093 100644 --- a/core/session-coordinator.ts +++ b/core/session-coordinator.ts @@ -1455,7 +1455,7 @@ export class SessionCoordinator { // C. restore=false → fresh compute from agent config // // allToolNames must cover the COMPLETE active set: Pi SDK built-ins - // (read/bash/edit/write/grep/find/ls) from sessionTools + HanaAgent + // (read/bash/edit/write/grep/find/ls) from sessionTools + Xiaoan // customs + plugin tools from sessionCustomTools. Using only agent.tools // would silently drop SDK built-ins and plugin tools when // setActiveToolsByName is applied. diff --git a/core/xiaoan-fork-migrate.ts b/core/xiaoan-fork-migrate.ts index 3b0456d2f..623bc475e 100644 --- a/core/xiaoan-fork-migrate.ts +++ b/core/xiaoan-fork-migrate.ts @@ -43,7 +43,7 @@ function saveConfig(configPath: string, raw: Record): void { /** * 对默认 agent(目录 id minian,legacy hanako)做一次性软迁移: - * - agent.name: Hanako/Hana → 小小安/Minian + * - agent.name: Hanako/Hana → 小安/Xiaoan * - agent.yuan: hanako → xiaoan(仅当 name 仍是 legacy 或未自定义 yuan) * - skills.enabled: 确保含 xiaoan-handbook */ diff --git a/desktop/auto-updater.cjs b/desktop/auto-updater.cjs index 860c3a1d9..70f04fb26 100644 --- a/desktop/auto-updater.cjs +++ b/desktop/auto-updater.cjs @@ -1,7 +1,7 @@ /** * auto-updater.cjs — electron-updater 集成 * - * 行为:启动时静默检查 → 静默下载 → renderer 展示状态 → 页内触发安装。 + * 行为:用户/偏好触发检查 → 静默下载 → renderer 展示状态 → 页内触发安装。 * Windows 安装时由 NSIS installer 负责关闭旧进程和覆盖安装;这里不等待 server * graceful shutdown,避免“重启更新”点击后长时间无反馈。 * 频道:Stable(allowPrerelease=false)/ Preview(allowPrerelease=true)。 @@ -23,7 +23,7 @@ let _updaterConfigured = false; let _installPromise = null; /** - * 读 preferences.json 里的 auto_check_updates,默认 true。 + * 读 preferences.json 里的 auto_check_updates,默认使用小安 fork 配置。 * 不缓存:每次调用都重新读,用户在设置页改完立刻生效, * 不用另起一条 main↔server 的 IPC 通道。 */ @@ -31,10 +31,10 @@ function isAutoCheckEnabled() { try { const prefsPath = path.join(_hanakoHome || "", "user", "preferences.json"); const prefs = JSON.parse(fs.readFileSync(prefsPath, "utf-8")); - return prefs.auto_check_updates !== false; + if (typeof prefs.auto_check_updates === "boolean") return prefs.auto_check_updates; } catch { - return true; } + return xiaoanFork.defaultAutoCheckUpdates === true; } // ── 状态管理(保持与前端 AutoUpdateState 契约一致)── @@ -399,7 +399,7 @@ function initAutoUpdater(mainWindow, { registerIpcHandlers(); // IPC handlers 是进程级单例,重复 init 时直接复用 - // 小安 fork 默认关闭自动更新(不拉取上游 OpenHanako 包) + // 小安 fork 的 feed 只指向本仓库;是否启用由 fork 配置/env 控制。 if (!isAutoUpdateEnabled()) { logUpdate(`auto-update disabled for ${xiaoanFork.productId} fork`); return; diff --git a/desktop/bootstrap.cjs b/desktop/bootstrap.cjs index fcd7bfb73..f9c5e607b 100644 --- a/desktop/bootstrap.cjs +++ b/desktop/bootstrap.cjs @@ -106,7 +106,7 @@ function recordProcessError(kind, err) { } process.on("uncaughtException", (err) => { - if (err?.code === "EPIPE" || err?.code === "ERR_IPC_CHANNEL_CLOSED") return; + if (err?.code === "EPIPE" || err?.code === "EIO" || err?.code === "ERR_IPC_CHANNEL_CLOSED") return; recordProcessError("uncaughtException", err); }); @@ -122,7 +122,8 @@ try { const { resolveHanakoHome, migrateDefaultHomeDirectory } = require("../shared/hana-runtime-paths.cjs"); BOOTSTRAP_PRODUCT = getMinianNameEn(); migrateDefaultHomeDirectory(); - hanakoHome = resolveHanakoHome(process.env.HANA_HOME); + hanakoHome = resolveHanakoHome(process.env.XIAOAN_HOME || process.env.HANA_HOME); + process.env.XIAOAN_HOME = hanakoHome; process.env.HANA_HOME = hanakoHome; diagnosticsDir = path.join(hanakoHome, "diagnostics", "desktop-launch"); } catch (err) { diff --git a/desktop/main.cjs b/desktop/main.cjs index 10690ff45..e6d4ca9be 100644 --- a/desktop/main.cjs +++ b/desktop/main.cjs @@ -1,9 +1,9 @@ /** - * HanaAgent Desktop — Electron 主进程 + * Xiaoan Desktop — Electron 主进程 * * 职责: * 1. 创建启动窗口(splash) - * 2. spawn() 启动 HanaAgent Server + * 2. spawn() 启动 Xiaoan Server * 3. 等待 server 就绪 + 主窗口初始化完成 * 4. 关闭 splash,显示主窗口 * 5. 优雅关闭 @@ -150,7 +150,8 @@ function safeReadJSON(filePath, fallback = null) { } } -const hanakoHome = resolveHanakoHome(process.env.HANA_HOME); +const hanakoHome = resolveHanakoHome(process.env.XIAOAN_HOME || process.env.HANA_HOME); +process.env.XIAOAN_HOME = hanakoHome; process.env.HANA_HOME = hanakoHome; ensureHanaPiSdkDirs(hanakoHome); configureProcessPiSdkEnv(hanakoHome); @@ -248,9 +249,9 @@ async function serverEnvironmentForNetworkProxy(baseEnv) { }, baseEnv, config); } -// 按 HANA_HOME 隔离 Electron userData(localStorage / cache / session) -// 生产: ~/Library/Application Support/Hanako(历史目录,随 HanaAgent 显示名保留) -// 开发: ~/Library/Application Support/Hanako-dev +// 按 Xiaoan/Hanako home 隔离 Electron userData(localStorage / cache / session) +// 生产: ~/Library/Application Support/Xiaoan +// 开发: ~/Library/Application Support/Xiaoan-dev(legacy HANA_HOME=.hanako-dev 时仍保持 Hanako-dev) const defaultHome = path.join(os.homedir(), ".hanako"); configureClientSingleInstance(app, { hanakoHome, @@ -985,6 +986,8 @@ async function startServer() { // 这里事先做退避检查,避免后续 spawn 出 ERR_MODULE_NOT_FOUND。 const bundledServerRoot = path.join(process.resourcesPath || "", "server"); const isBundledMode = + fs.existsSync(path.join(bundledServerRoot, "xiaoan-server")) || + fs.existsSync(path.join(bundledServerRoot, "xiaoan-server.exe")) || fs.existsSync(path.join(bundledServerRoot, "hana-server")) || fs.existsSync(path.join(bundledServerRoot, "hana-server.exe")); if (isBundledMode) { @@ -1078,12 +1081,21 @@ async function _spawnServerOnce(serverInfoPath) { // 选择 server 启动方式 let serverBin, serverArgs; const bundledServerRoot = path.join(process.resourcesPath || "", "server"); - const bundledServer = path.join(bundledServerRoot, "hana-server"); - if (fs.existsSync(bundledServer) || fs.existsSync(bundledServer + ".exe")) { + const bundledServer = path.join(bundledServerRoot, "xiaoan-server"); + const legacyBundledServer = path.join(bundledServerRoot, "hana-server"); + if ( + fs.existsSync(bundledServer) || + fs.existsSync(bundledServer + ".exe") || + fs.existsSync(legacyBundledServer) || + fs.existsSync(legacyBundledServer + ".exe") + ) { // 打包模式:使用 extraResources 里的独立 server - // macOS/Linux:hana-server 是 shell wrapper,内部调用 bootstrap.js,无需额外参数 - // Windows:hana-server.exe 是裸 Node 二进制(改名),需要显式传入 bootstrap.js - const bin = process.platform === "win32" ? bundledServer + ".exe" : bundledServer; + // macOS/Linux:xiaoan-server 是 shell wrapper,内部调用 bootstrap.js,无需额外参数 + // Windows:xiaoan-server.exe 是裸 Node 二进制(改名),需要显式传入 bootstrap.js + const resolvedBundledServer = fs.existsSync(bundledServer) || fs.existsSync(bundledServer + ".exe") + ? bundledServer + : legacyBundledServer; + const bin = process.platform === "win32" ? resolvedBundledServer + ".exe" : resolvedBundledServer; const entry = path.join(bundledServerRoot, "bundle", "index.js"); serverBin = bin; serverArgs = process.platform === "win32" @@ -1225,7 +1237,7 @@ function showPrimaryWindow() { /** * 创建系统托盘图标 * - 双击:显示主窗口 - * - 右键菜单:显示 HanaAgent / 设置 / 退出 + * - 右键菜单:显示小安 / 设置 / 退出 */ function resolveTrayAssetCandidates(fileName) { const candidates = []; @@ -1319,14 +1331,18 @@ function buildServerCrashDiagnostics() { // Windows: 检查 server 二进制、手动调试 wrapper 和 PortableGit if (process.platform === "win32" && isPackaged) { - const exePath = path.join(serverDir, "hana-server.exe"); - const cmdPath = path.join(serverDir, "hana-server.cmd"); + const exePath = path.join(serverDir, "xiaoan-server.exe"); + const legacyExePath = path.join(serverDir, "hana-server.exe"); + const cmdPath = path.join(serverDir, "xiaoan-server.cmd"); + const legacyCmdPath = path.join(serverDir, "hana-server.cmd"); const gitRoot = path.join(process.resourcesPath, "git"); - items.push(`hana-server.exe exists: ${fs.existsSync(exePath)}`); - items.push(`hana-server.cmd exists (manual debug): ${fs.existsSync(cmdPath)}`); + items.push(`xiaoan-server.exe exists: ${fs.existsSync(exePath)}`); + items.push(`hana-server.exe exists (legacy): ${fs.existsSync(legacyExePath)}`); + items.push(`xiaoan-server.cmd exists (manual debug): ${fs.existsSync(cmdPath)}`); + items.push(`hana-server.cmd exists (legacy debug): ${fs.existsSync(legacyCmdPath)}`); items.push(`PortableGit dir exists: ${fs.existsSync(gitRoot)}`); items.push(``); - items.push(`Manual debug: open cmd.exe, cd to "${serverDir}", run hana-server.cmd`); + items.push(`Manual debug: open cmd.exe, cd to "${serverDir}", run xiaoan-server.cmd`); } items.push(buildGpuStartupDiagnostics({ hanakoHome, policy: gpuStartupPolicy, app })); @@ -4676,7 +4692,7 @@ app.whenReady().then(async () => { // 3. 确保 splash 至少显示 3 秒;登录项后台启动没有 splash,也不需要等待 const elapsed = Date.now() - splashShownAt; - const minSplashMs = 3000; + const minSplashMs = 900; if (splashWindow && elapsed < minSplashMs) { await new Promise(r => setTimeout(r, minSplashMs - elapsed)); } @@ -4896,17 +4912,32 @@ app.on("before-quit", async (event) => { } }); +function isProcessOutputClosedError(err) { + const code = err && err.code; + return code === "EPIPE" || code === "EIO" || code === "ERR_IPC_CHANNEL_CLOSED" || code === "ERR_STREAM_DESTROYED"; +} + +function safeConsoleError(line) { + try { + console.error(line); + } catch { + return false; + } + return true; +} + // ── 全局错误兜底(结构化日志)── process.on('uncaughtException', (err) => { - if (err.code === 'EPIPE' || err.code === 'ERR_IPC_CHANNEL_CLOSED') return; + if (isProcessOutputClosedError(err)) return; const traceId = Math.random().toString(16).slice(2, 10); - console.error(`[ErrorBus][${err.code || 'UNKNOWN'}][${traceId}] uncaughtException: ${redactMainLogText(err.message)}`); - console.error(`[ErrorBus][${traceId}] ${redactMainLogText(err.stack || err.message)}`); + safeConsoleError(`[ErrorBus][${err.code || 'UNKNOWN'}][${traceId}] uncaughtException: ${redactMainLogText(err.message)}`); + safeConsoleError(`[ErrorBus][${traceId}] ${redactMainLogText(err.stack || err.message)}`); }); process.on('unhandledRejection', (reason) => { const err = reason instanceof Error ? reason : new Error(String(reason)); + if (isProcessOutputClosedError(err)) return; const traceId = Math.random().toString(16).slice(2, 10); - console.error(`[ErrorBus][${err.code || 'UNKNOWN'}][${traceId}] unhandledRejection: ${redactMainLogText(err.message)}`); - console.error(`[ErrorBus][${traceId}] ${redactMainLogText(err.stack || err.message)}`); + safeConsoleError(`[ErrorBus][${err.code || 'UNKNOWN'}][${traceId}] unhandledRejection: ${redactMainLogText(err.message)}`); + safeConsoleError(`[ErrorBus][${traceId}] ${redactMainLogText(err.stack || err.message)}`); }); diff --git a/desktop/preload.cjs b/desktop/preload.cjs index d92fd4229..9ae46e9c1 100644 --- a/desktop/preload.cjs +++ b/desktop/preload.cjs @@ -1,5 +1,5 @@ /** - * Hana Desktop — Preload 桥接 + * Xiaoan Desktop — Preload 桥接 * * 业务通信走 HTTP/WS 到 server。 * IPC 仅用于:窗口管理、系统对话框、跨窗口消息转发。 @@ -19,7 +19,7 @@ function resolveTheme() { return themeRegistry.resolveSavedTheme(saved, isDark).concrete; } -contextBridge.exposeInMainWorld("hana", { +const xiaoanBridge = { getServerPort: () => ipcRenderer.invoke("get-server-port"), getServerToken: () => ipcRenderer.invoke("get-server-token"), runEditCommand: (command) => ipcRenderer.invoke("run-edit-command", command), @@ -158,4 +158,7 @@ contextBridge.exposeInMainWorld("hana", { ipcRenderer.on("window-maximized", () => cb(true)); ipcRenderer.on("window-unmaximized", () => cb(false)); }, -}); +}; + +contextBridge.exposeInMainWorld("xiaoan", xiaoanBridge); +contextBridge.exposeInMainWorld("hana", xiaoanBridge); diff --git a/desktop/src/assets/Butter.png b/desktop/src/assets/Butter.png index fd9438533..d97f4e905 100644 Binary files a/desktop/src/assets/Butter.png and b/desktop/src/assets/Butter.png differ diff --git a/desktop/src/assets/Ming.png b/desktop/src/assets/Ming.png index 7102adf9d..a3f62e97d 100644 Binary files a/desktop/src/assets/Ming.png and b/desktop/src/assets/Ming.png differ diff --git a/desktop/src/design/xiaoan-console-shell.css b/desktop/src/design/xiaoan-console-shell.css index f89951ebc..2f2ac63d5 100644 --- a/desktop/src/design/xiaoan-console-shell.css +++ b/desktop/src/design/xiaoan-console-shell.css @@ -2,77 +2,205 @@ * 小安控制台壳层 — 视觉 chrome(hairline / 卡片);排版见 xiaoan-layout.css */ -html[data-theme="xiaoan-console"], -html[data-theme="xiaoan-console-dark"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"], [data-theme="xiaoan-claude-dark"]) { --xiaoan-console-grid: transparent; - --xiaoan-console-hairline: var(--console-hairline, rgba(0, 0, 0, 0.08)); + --xiaoan-console-hairline: var(--console-hairline); --xiaoan-console-accent-bar: var(--accent); } -html[data-theme="xiaoan-console"] body, -html[data-theme="xiaoan-console-dark"] body { - font-family: var(--font-ui); +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) body, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) body { + font-family: var(--font-serif); -webkit-font-smoothing: antialiased; background: var(--bg); background-image: none; } -html[data-theme="xiaoan-console-dark"] { - --xiaoan-console-hairline: var(--console-hairline, rgba(255, 255, 255, 0.08)); -} - -html[data-theme="xiaoan-console"] .universal-card, -html[data-theme="xiaoan-console-dark"] .universal-card { - border-radius: var(--radius-lg); - box-shadow: var(--panel-card-shadow); - border: 1px solid var(--border); +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .universal-card, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .universal-card { + border-radius: 0; + box-shadow: none; + border: var(--panel-card-border); } -html[data-theme="xiaoan-console"] [data-gsap-session-item][data-active="true"], -html[data-theme="xiaoan-console-dark"] [data-gsap-session-item][data-active="true"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-gsap-session-item][data-active="true"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-gsap-session-item][data-active="true"] { outline: none; background: var(--sidebar-active-bg); - border-radius: 8px; + border-radius: 0; } -html[data-theme="xiaoan-console"] .connection-status.connected .status-dot, -html[data-theme="xiaoan-console-dark"] .connection-status.connected .status-dot { - background: var(--accent); +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .connection-status.connected .status-dot, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .connection-status.connected .status-dot { + background: var(--green); + border-radius: 0; + width: 6px; + height: 6px; box-shadow: none; } -html[data-theme="xiaoan-console"] .status-bar, -html[data-theme="xiaoan-console-dark"] .status-bar, -html[data-theme="xiaoan-console"] [class*="StatusBar_bar"], -html[data-theme="xiaoan-console-dark"] [class*="StatusBar_bar"] { - border-bottom: 1px solid var(--border); +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .status-bar, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .status-bar, +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [class*="StatusBar_bar"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [class*="StatusBar_bar"] { + border-bottom: var(--border-width) solid var(--console-hairline); background: var(--bg-card); } .xiaoan-gsap-progress { position: relative; overflow: hidden; + height: 1px; + background: var(--console-hairline); } .xiaoan-gsap-progress__bar { height: 100%; width: 0%; transform-origin: left center; + border-radius: 0; + background: var(--accent); + box-shadow: none; } .xiaoan-gsap-progress[data-active="true"] .xiaoan-gsap-progress__bar { - animation: xiaoan-console-scan 1.4s ease-in-out infinite; -} - -@keyframes xiaoan-console-scan { - 0% { transform: translateX(-100%) scaleX(0.35); } - 50% { transform: translateX(20%) scaleX(0.55); } - 100% { transform: translateX(200%) scaleX(0.35); } + animation: none; } @media (prefers-reduced-motion: reduce) { .xiaoan-gsap-progress[data-active="true"] .xiaoan-gsap-progress__bar { - animation: none; width: 40%; } + + html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="xiaoan-claude-dark"]) .app-shell, + html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="xiaoan-claude-dark"]) .app-shell .titlebar, + html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="xiaoan-claude-dark"]) .app-shell .sidebar, + html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="xiaoan-claude-dark"]) .app-shell .main-content, + html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="xiaoan-claude-dark"]) [data-chat-shell] [data-gsap-chat-enter] { + opacity: 1 !important; + transform: none !important; + visibility: visible !important; + } +} + +/* 直角输入框:1px 细线、无阴影 */ +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .input-wrapper, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .input-wrapper { + border: var(--border-width) solid var(--border); + border-radius: 0; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .input-wrapper:focus-within, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .input-wrapper:focus-within { + border-color: var(--brand); + box-shadow: none; + outline: var(--border-width) solid var(--brand); + outline-offset: -1px; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .input-wrapper:hover, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .input-wrapper:hover { + border-color: var(--border); + box-shadow: none; +} + +/* 主操作:实心黑按钮(浅色)/ 白按钮(深色) */ +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .send-btn:not(.is-streaming) { + background: var(--brand); + border-color: var(--brand); + color: #ffffff; + border-radius: 0; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .send-btn:not(.is-streaming):hover:not(:disabled) { + background: var(--brand-hover); + border-color: var(--brand-hover); + filter: none; +} + +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .send-btn:not(.is-streaming) { + background: #f5f5f5; + border-color: #f5f5f5; + color: #171717; +} + +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .send-btn:not(.is-streaming):hover:not(:disabled) { + background: #ffffff; + border-color: #ffffff; + filter: none; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .send-btn.is-streaming, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .send-btn.is-streaming { + color: var(--accent); + border-color: rgba(var(--accent-rgb), 0.45); +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .send-btn.is-streaming:hover, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .send-btn.is-streaming:hover { + background: rgba(var(--accent-rgb), 0.12); + color: var(--accent-hover); + border-color: var(--accent); +} + +/* 会话搜索框 */ +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .sessionSearchBox input, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .sessionSearchBox input { + border-radius: 0; + border: var(--border-width) solid var(--border); + background: var(--bg-card); + box-shadow: none; +} + +/* 用户气泡:浅灰卡片感 */ +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .messageUser, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .messageUser { + border: var(--border-width) solid var(--border); + border-radius: 0; + box-shadow: none; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .messageAssistant :global(.md-content), +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .messageAssistant :global(.md-content) { + font-family: var(--font-serif); + line-height: var(--chat-message-line-height); +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .sidebar-header, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .sidebar-header { + padding: 6px 8px; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .sidebar, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .sidebar { + border-right: var(--border-width) solid var(--console-hairline); +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .titlebar, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .titlebar { + border-bottom: var(--border-width) solid var(--console-hairline); + background: var(--bg-card); +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .main-content:not(.welcome-mode), +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .main-content:not(.welcome-mode) { + background: var(--bg-card); +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-chat-shell] .input-area, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-chat-shell] .input-area { + background: var(--bg-card); +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="assistant"] [data-layout="avatar-row"] img, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="assistant"] [data-layout="avatar-row"] img { + border-radius: 0; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .xiaoan-platform-brand__avatar, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .xiaoan-platform-brand__avatar { + border-radius: 0; } diff --git a/desktop/src/design/xiaoan-layout.css b/desktop/src/design/xiaoan-layout.css index eac28b529..f2731bf2e 100644 --- a/desktop/src/design/xiaoan-layout.css +++ b/desktop/src/design/xiaoan-layout.css @@ -1,109 +1,109 @@ /** * 小安控制台 · 排版系统(Layout System) * - * 参考 Hermes Studio 的信息架构,仅负责几何与流式布局,不含颜色/圆角/阴影。 + * 参考 Alice / Kimi / Claude Desktop / ChatGPT 桌面端的共性: + * - 左栏会话轨 + 中间居中阅读列(≈720px)+ 底栏输入同宽对齐 + * - Welcome 垂直居中、输入与标题同列 + * - 用户气泡右对齐(~78%),助手头像 + 全列正文 + * - flex-footer 输入(非 overlay) + * + * 仅负责几何与流式布局,不含颜色/圆角/阴影。 * * 层级模型 * ───────────────────────────────────────── * L0 app-shell titlebar + 横向 app - * L1 sidebar 240px 会话轨 + * L1 sidebar 会话轨(260px) * L2 main-content flex 列,承载 chat / settings 等 - * L3 [data-chat-shell] 对话壳:滚动区 + 输入 footer(非 overlay) - * L4 session-* 消息滚动面板 + 消息列 - * L5 [data-message-role] 单条消息对齐(user 75% / assistant 头像+80%) - * - * 锚点约定(React 侧 data-layout / data-message-role) - * ───────────────────────────────────────── - * data-chat-shell ChatShell 根 - * data-layout="session-shell|session-scroll|session-messages" - * data-layout="session-list|welcome" - * data-message-role="user|assistant" - * data-layout="avatar-row|message-body|message-footer" + * L3 [data-chat-shell] 对话壳:滚动区 + 输入 footer + * L4 session-* 消息滚动面板 + 居中消息列 + * L5 [data-message-role] 单条消息对齐 */ -html[data-theme="xiaoan-console"], -html[data-theme="xiaoan-console-dark"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"], [data-theme="xiaoan-claude-dark"]) { /* L0–L1 壳层 */ - --xiaoan-layout-sidebar-w: 240px; - --xiaoan-layout-gutter: 20px; - --xiaoan-layout-sidebar-pad: 8px 12px 20px; + --xiaoan-layout-sidebar-w: 260px; + --xiaoan-layout-gutter: 24px; + --xiaoan-layout-sidebar-pad: 10px 12px 16px; - /* L3 输入 footer */ - --xiaoan-layout-input-pad-y-start: 12px; - --xiaoan-layout-input-pad-y-end: 16px; + /* L3 居中对话列(与 settings 中 chatWidth 联动 via --chat-column-width) */ + --xiaoan-layout-welcome-max: min(42rem, 100%); + --xiaoan-layout-input-pad-y-start: 14px; + --xiaoan-layout-input-pad-y-end: 18px; - /* L4 消息区 */ - --xiaoan-layout-msg-gap: 16px; + /* L4 消息区 — Alice 式阅读节奏 */ + --xiaoan-layout-msg-gap: 20px; + --xiaoan-layout-session-scroll-pad-top: 28px; + --xiaoan-layout-session-scroll-pad-bottom: 20px; /* L5 气泡对齐 */ - --xiaoan-layout-msg-user-max: 75%; - --xiaoan-layout-msg-assistant-max: 80%; - --xiaoan-layout-avatar-size: 40px; - --xiaoan-layout-avatar-gap: 8px; - - /* L1 会话列表 */ - --xiaoan-layout-session-list-pad: 10px 6px 12px; - --xiaoan-layout-session-item-pad: 12px; + --xiaoan-layout-msg-user-max: 78%; + --xiaoan-layout-msg-assistant-max: 100%; + --xiaoan-layout-avatar-size: 36px; + --xiaoan-layout-avatar-gap: 10px; + + /* L1 会话列表 — 大厂桌面端偏紧凑但不挤 */ + --xiaoan-layout-session-list-pad: 6px 8px 10px; + --xiaoan-layout-session-item-pad: 10px 12px; --xiaoan-layout-session-section-pad: 8px 12px 4px; /* Welcome */ - --xiaoan-layout-welcome-max: min(40rem, 100%); - --xiaoan-layout-welcome-top: calc(10vh + 1rem); + --xiaoan-layout-welcome-top: calc(12vh + 0.5rem); - /* 时间轴 / FAB(flex footer 模式下不再叠在输入框上) */ - --xiaoan-layout-timeline-top: 20px; + /* 时间轴 / FAB */ + --xiaoan-layout-timeline-top: 24px; --xiaoan-layout-scroll-fab-bottom: var(--space-md); - /* 输入模式 + 桥接全局 session/FAB token(见 styles.css :root 默认值) */ + /* 输入模式 + 桥接全局 session/FAB token */ --xiaoan-chat-input-layout: flex-footer; --chat-scrollbar-bottom-inset: 0; --session-shell-bottom: 0; - --session-panel-padding: var(--xiaoan-layout-gutter); - --session-messages-max-width: 100%; + --session-panel-padding: var(--xiaoan-layout-gutter) + var(--xiaoan-layout-gutter) + var(--xiaoan-layout-session-scroll-pad-bottom); + --session-messages-max-width: var(--chat-column-width); --session-messages-gap: var(--xiaoan-layout-msg-gap); --scroll-fab-bottom: var(--xiaoan-layout-scroll-fab-bottom); + --welcome-chat-input-column-width: var(--xiaoan-layout-welcome-max); - /* 桥接到既有 token(其他 CSS 可继续引用) */ + /* 桥接到既有 token */ --sidebar-width: var(--xiaoan-layout-sidebar-w); --console-content-gutter: var(--xiaoan-layout-gutter); --chat-msg-group-gap: var(--xiaoan-layout-msg-gap); - --chat-input-column-width: 100%; - --welcome-chat-input-column-width: var(--xiaoan-layout-welcome-max); } /* ═══════════════════════════════════════ L0–L2 App 骨架 ═══════════════════════════════════════ */ -html[data-theme="xiaoan-console"] .app, -html[data-theme="xiaoan-console-dark"] .app { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .app, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .app { padding: 0; gap: 0; } -html[data-theme="xiaoan-console"] .titlebar, -html[data-theme="xiaoan-console-dark"] .titlebar { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .titlebar, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .titlebar { margin: 0; width: 100%; border-radius: 0; } -html[data-theme="xiaoan-console"] .sidebar, -html[data-theme="xiaoan-console-dark"] .sidebar { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .sidebar, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .sidebar { width: var(--xiaoan-layout-sidebar-w); padding: var(--xiaoan-layout-sidebar-pad); border-radius: 0; } -html[data-theme="xiaoan-console"] .main-content, -html[data-theme="xiaoan-console-dark"] .main-content { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .main-content, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .main-content { padding: 0; border-radius: 0; min-height: 0; } -html[data-theme="xiaoan-console"] .jian-sidebar-inner, -html[data-theme="xiaoan-console-dark"] .jian-sidebar-inner { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .jian-sidebar-inner, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .jian-sidebar-inner { border-radius: 0; } @@ -111,15 +111,15 @@ html[data-theme="xiaoan-console-dark"] .jian-sidebar-inner { L3 Chat shell:flex 列 = 滚动 + footer ═══════════════════════════════════════ */ -html[data-theme="xiaoan-console"] [data-chat-shell], -html[data-theme="xiaoan-console-dark"] [data-chat-shell] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-chat-shell], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-chat-shell] { flex: 1; min-height: 0; border-radius: 0; } -html[data-theme="xiaoan-console"] [data-chat-shell] .chat-area, -html[data-theme="xiaoan-console-dark"] [data-chat-shell] .chat-area { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-chat-shell] .chat-area, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-chat-shell] .chat-area { flex: 1; min-height: 0; max-width: none; @@ -128,97 +128,117 @@ html[data-theme="xiaoan-console-dark"] [data-chat-shell] .chat-area { overflow: hidden; } -/* 无 session 时(welcome):chat-area 自身滚动 + gutter */ -html[data-theme="xiaoan-console"] [data-chat-shell] .chat-area:not(.has-panels), -html[data-theme="xiaoan-console-dark"] [data-chat-shell] .chat-area:not(.has-panels) { +/* Welcome:chat-area 自身滚动,内容居中 */ +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-chat-shell] .chat-area:not(.has-panels), +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-chat-shell] .chat-area:not(.has-panels) { overflow-y: auto; padding: var(--xiaoan-layout-gutter); padding-bottom: var(--space-md); } -html[data-theme="xiaoan-console"] [data-chat-shell] .input-area, -html[data-theme="xiaoan-console-dark"] [data-chat-shell] .input-area { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-chat-shell] .input-area, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-chat-shell] .input-area { position: relative; bottom: auto; left: auto; right: auto; flex-shrink: 0; padding: var(--xiaoan-layout-input-pad-y-start) var(--xiaoan-layout-gutter) var(--xiaoan-layout-input-pad-y-end); - border-top: 1px solid var(--xiaoan-console-hairline); + border-top: var(--border-width, 1px) solid var(--xiaoan-console-hairline); background: var(--bg); pointer-events: auto; z-index: 1; } -html[data-theme="xiaoan-console"] [data-chat-shell] .input-area > *, -html[data-theme="xiaoan-console-dark"] [data-chat-shell] .input-area > * { - max-width: 100%; - margin-inline: 0; +/* Alice 式:输入框与消息列同宽居中(welcome + 会话内一致) */ +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-chat-shell] .input-area > *, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-chat-shell] .input-area > * { + width: 100%; + max-width: var(--chat-input-column-width); + margin-inline: auto; } -html[data-theme="xiaoan-console"] .main-content.welcome-mode .chat-area, -html[data-theme="xiaoan-console-dark"] .main-content.welcome-mode .chat-area { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .main-content.welcome-mode .chat-area, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .main-content.welcome-mode .chat-area { padding-top: var(--xiaoan-layout-welcome-top); } -html[data-theme="xiaoan-console"] .main-content.welcome-mode .input-area > *, -html[data-theme="xiaoan-console-dark"] .main-content.welcome-mode .input-area > * { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .main-content.welcome-mode [data-chat-shell] .input-area > *, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .main-content.welcome-mode [data-chat-shell] .input-area > * { max-width: var(--xiaoan-layout-welcome-max); - margin-inline: auto; +} + +/* ═══════════════════════════════════════ + L4 Session 滚动区 + ═══════════════════════════════════════ */ + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-layout="session-scroll"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-layout="session-scroll"] { + padding-top: var(--xiaoan-layout-session-scroll-pad-top); +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-layout="session-messages"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-layout="session-messages"] { + width: 100%; } /* ═══════════════════════════════════════ L5 消息对齐 ═══════════════════════════════════════ */ -html[data-theme="xiaoan-console"] [data-message-role], -html[data-theme="xiaoan-console-dark"] [data-message-role] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role] { max-width: 100%; margin-inline: 0; width: 100%; } -html[data-theme="xiaoan-console"] [data-message-role="user"] > *, -html[data-theme="xiaoan-console-dark"] [data-message-role="user"] > * { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="user"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="user"] { + align-items: flex-end; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="user"] > *, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="user"] > * { max-width: var(--xiaoan-layout-msg-user-max); } -html[data-theme="xiaoan-console"] [data-message-role="assistant"], -html[data-theme="xiaoan-console-dark"] [data-message-role="assistant"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="assistant"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="assistant"] { display: grid; grid-template-columns: var(--xiaoan-layout-avatar-size) minmax(0, 1fr); column-gap: var(--xiaoan-layout-avatar-gap); align-items: start; } -html[data-theme="xiaoan-console"] [data-message-role="assistant"] [data-layout="avatar-row"], -html[data-theme="xiaoan-console-dark"] [data-message-role="assistant"] [data-layout="avatar-row"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="assistant"] [data-layout="avatar-row"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="assistant"] [data-layout="avatar-row"] { grid-column: 1; grid-row: 1; margin-top: 2px; margin-bottom: 0; } -html[data-theme="xiaoan-console"] [data-message-role="assistant"] [data-layout="avatar-row"] [data-layout="avatar-name"], -html[data-theme="xiaoan-console-dark"] [data-message-role="assistant"] [data-layout="avatar-row"] [data-layout="avatar-name"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="assistant"] [data-layout="avatar-row"] [data-layout="avatar-name"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="assistant"] [data-layout="avatar-row"] [data-layout="avatar-name"] { display: none; } -html[data-theme="xiaoan-console"] [data-message-role="assistant"] [data-layout="message-body"], -html[data-theme="xiaoan-console-dark"] [data-message-role="assistant"] [data-layout="message-body"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="assistant"] [data-layout="message-body"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="assistant"] [data-layout="message-body"] { grid-column: 2; grid-row: 1; max-width: var(--xiaoan-layout-msg-assistant-max); } -html[data-theme="xiaoan-console"] [data-message-role="assistant"] [data-layout="message-footer"], -html[data-theme="xiaoan-console-dark"] [data-message-role="assistant"] [data-layout="message-footer"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="assistant"] [data-layout="message-footer"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="assistant"] [data-layout="message-footer"] { grid-column: 2; grid-row: 2; } -html[data-theme="xiaoan-console"] [data-message-role="assistant"] [data-layout="avatar-row"] img, -html[data-theme="xiaoan-console-dark"] [data-message-role="assistant"] [data-layout="avatar-row"] img { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-message-role="assistant"] [data-layout="avatar-row"] img, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-message-role="assistant"] [data-layout="avatar-row"] img { width: var(--xiaoan-layout-avatar-size); height: var(--xiaoan-layout-avatar-size); } @@ -227,31 +247,34 @@ html[data-theme="xiaoan-console-dark"] [data-message-role="assistant"] [data-lay L1 侧栏会话列表 ═══════════════════════════════════════ */ -html[data-theme="xiaoan-console"] [data-layout="session-list"], -html[data-theme="xiaoan-console-dark"] [data-layout="session-list"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-layout="session-list"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-layout="session-list"] { padding: var(--xiaoan-layout-session-list-pad); } -html[data-theme="xiaoan-console"] [data-layout="session-section-title"], -html[data-theme="xiaoan-console-dark"] [data-layout="session-section-title"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-layout="session-section-title"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-layout="session-section-title"] { padding: var(--xiaoan-layout-session-section-pad); + font-size: 0.68rem; + letter-spacing: 0.06em; + text-transform: uppercase; } -html[data-theme="xiaoan-console"] [data-layout="session-item"], -html[data-theme="xiaoan-console-dark"] [data-layout="session-item"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-layout="session-item"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-layout="session-item"] { padding: var(--xiaoan-layout-session-item-pad); - margin-bottom: 2px; + margin-bottom: 1px; } /* ═══════════════════════════════════════ Welcome 空态 ═══════════════════════════════════════ */ -html[data-theme="xiaoan-console"] [data-layout="welcome"], -html[data-theme="xiaoan-console-dark"] [data-layout="welcome"] { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) [data-layout="welcome"], +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) [data-layout="welcome"] { max-width: var(--xiaoan-layout-welcome-max); width: 100%; margin-inline: auto; - gap: 12px; + gap: 1rem; padding-inline: 0; } diff --git a/desktop/src/design/xiaoan-platform.css b/desktop/src/design/xiaoan-platform.css index 72e23f97b..aee569513 100644 --- a/desktop/src/design/xiaoan-platform.css +++ b/desktop/src/design/xiaoan-platform.css @@ -39,7 +39,7 @@ .xiaoan-platform-brand__name { font-size: 0.82rem; font-weight: 600; - letter-spacing: 0.04em; + letter-spacing: -0.01em; color: var(--text); white-space: nowrap; } @@ -76,52 +76,57 @@ font-weight: 600; } -html[data-theme="xiaoan-console"] .sidebar-header, -html[data-theme="xiaoan-console-dark"] .sidebar-header { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .sidebar-header, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .sidebar-header { align-items: center; gap: 0.35rem; } -html[data-theme="xiaoan-console"] .sidebar-header-actions, -html[data-theme="xiaoan-console-dark"] .sidebar-header-actions { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .sidebar-header-actions, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .sidebar-header-actions { margin-left: auto; } -/* 控制台 sans 排版微调 */ -html[data-theme="xiaoan-console"] .xiaoan-platform-brand__name, -html[data-theme="xiaoan-console-dark"] .xiaoan-platform-brand__name { - font-family: var(--font-ui); +/* 控制台衬线排版 */ +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .xiaoan-platform-brand__name, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .xiaoan-platform-brand__name { + font-family: var(--font-serif); font-weight: 600; - letter-spacing: 0.02em; + letter-spacing: -0.01em; } -html[data-theme="xiaoan-console"] .xiaoan-platform-brand__tagline, -html[data-theme="xiaoan-console-dark"] .xiaoan-platform-brand__tagline { - font-family: var(--font-ui); +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) body, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) body { + letter-spacing: 0.005em; +} + +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .xiaoan-platform-brand__tagline, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .xiaoan-platform-brand__tagline { + font-family: var(--font-serif); letter-spacing: 0; font-size: 0.62rem; } -html[data-theme="xiaoan-console"] .input-wrapper :global(.ProseMirror), -html[data-theme="xiaoan-console-dark"] .input-wrapper :global(.ProseMirror) { - font-family: var(--font-ui); - letter-spacing: 0.01em; - line-height: 1.65; +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .input-wrapper :global(.ProseMirror), +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .input-wrapper :global(.ProseMirror) { + font-family: var(--font-serif); + letter-spacing: 0.005em; + line-height: 1.55; } -html[data-theme="xiaoan-console"] .input-wrapper :global(.ProseMirror p.is-editor-empty:first-child::before), -html[data-theme="xiaoan-console-dark"] .input-wrapper :global(.ProseMirror p.is-editor-empty:first-child::before) { - font-family: var(--font-ui); +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .input-wrapper :global(.ProseMirror p.is-editor-empty:first-child::before), +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .input-wrapper :global(.ProseMirror p.is-editor-empty:first-child::before) { + font-family: var(--font-serif); letter-spacing: 0; opacity: 0.45; } -html[data-theme="xiaoan-console"] .sessionSearchBox input, -html[data-theme="xiaoan-console-dark"] .sessionSearchBox input { - font-family: var(--font-ui); +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .sessionSearchBox input, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .sessionSearchBox input { + font-family: var(--font-serif); } -html[data-theme="xiaoan-console"] .sidebar-activity-bar span, -html[data-theme="xiaoan-console-dark"] .sidebar-activity-bar span { +html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"]) .sidebar-activity-bar span, +html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"]) .sidebar-activity-bar span { letter-spacing: 0; } diff --git a/desktop/src/global.d.ts b/desktop/src/global.d.ts index f46d13547..306e8f398 100644 --- a/desktop/src/global.d.ts +++ b/desktop/src/global.d.ts @@ -1,5 +1,5 @@ /** - * Hana Desktop — 全局类型声明 + * Xiaoan Desktop — 全局类型声明 * * 集中声明 window 上的全局属性,避免散落的 `(window as any)` 和重复的 declare global。 */ @@ -13,6 +13,7 @@ declare global { // ── Platform bridge(preload 注入) ── platform: PlatformApi; + xiaoan: PlatformApi; hana: PlatformApi; // ── 日志上报 ── diff --git a/desktop/src/locales/en.json b/desktop/src/locales/en.json index 910435352..266c654ec 100644 --- a/desktop/src/locales/en.json +++ b/desktop/src/locales/en.json @@ -6,9 +6,9 @@ "defaultModelMissing": "Configure a default image model in settings first.", "defaultModelInvalid": "The default image model is unavailable. Reconfigure it in settings.", "imageGenUnavailable": "The image generation tool is disabled. Enable it in settings first.", - "agentUnavailable": "The main agent is temporarily unavailable, so Agent generation can't start.", - "disabled": "Agent generation is currently unavailable.", - "label": "Agent generate" + "agentUnavailable": "The primary assistant is temporarily unavailable, so Xiaoan cannot generate the cover.", + "disabled": "Xiaoan generation is currently unavailable.", + "label": "Xiaoan generate" }, "gallery": { "title": "Cover gallery", @@ -21,16 +21,16 @@ }, "platform": { "name": "Xiaoan", - "tagline": "Your local AI assistant", + "tagline": "Your local AI partner", "sidebarTagline": "Chats & workspace", "hints": { - "brief": "Brief · clarify intent", - "plan": "Plan · set the order", - "desk": "Desk · write together" + "brief": "Clarify what you need", + "plan": "Break it into steps", + "desk": "Write together in the workspace" }, "empty": { - "title": "No chapter yet", - "hint": "Begin a new conversation, or file your thoughts in a project" + "title": "No chats yet", + "hint": "Start a new chat or pick a workspace folder" } }, "mermaid": { @@ -54,27 +54,27 @@ "moreTabs": "More tabs", "empty": "No channels yet", "dmLabel": "Messages", - "dmHint": "Primary Agent Only", + "dmHint": "Primary Xiaoan mode only", "groupLabel": "Channels", "noMessages": "No messages yet", "selectHint": "Select a channel or DM to start", "nameExists": "This channel name already exists", "membersCount": " members", "info": "Channel info", - "readOnly": "🔒 This is a private message between agents, view only", + "readOnly": "🔒 This is a private message between Xiaoan modes, view only", "inputPlaceholder": "Send a message...", "newMessages": "New messages", "sendFailed": "Send failed", "createTitle": "New Channel", "createName": "Channel name", "createNamePlaceholder": "e.g. design-team", - "createMembers": "Agent members", + "createMembers": "Mode members", "createIntro": "Description", "createIntroPlaceholder": "Describe what this channel is for...", "createCancel": "Cancel", "createConfirm": "Create", "nameRequired": "Please enter a channel name", - "minMembers": "Select at least 2 Agent members", + "minMembers": "Select at least 2 Xiaoan modes", "created": "Channel #{name} created", "createFailed": "Create failed", "alreadyExists": "Channel already exists", @@ -83,7 +83,7 @@ "deleted": "Channel deleted", "deleteFailed": "Delete failed", "warningTitle": "About Channels", - "warningBody": "I believe letting assistants with different memories and thinking styles interact can spark more inspiration, so I built a place where agents can talk to each other, even when you're away.\n\nPlease note that channels are experimental and may cause:\n1. Higher token usage\n2. Unexpected responses\n3. Memory confusion\n\nUse with caution", + "warningBody": "I believe letting Xiaoan modes with different memories and thinking styles interact can spark more inspiration, so I built a place where they can talk to each other, even when you're away.\n\nPlease note that channels are experimental and may cause:\n1. Higher token usage\n2. Unexpected responses\n3. Memory confusion\n\nUse with caution", "warningConfirm": "I understand, enable channels", "disabled": "Channels disabled", "toggleLabel": "Channel toggle", @@ -93,8 +93,8 @@ "removeMemberConfirm": "Remove \"{name}\" from this channel?", "addMemberFailed": "Failed to add member", "removeMemberFailed": "Failed to remove member", - "noAvailableMembers": "No agents available", - "agentActivity": "Agent activity", + "noAvailableMembers": "No Xiaoan modes available", + "agentActivity": "Mode activity", "agentSettings": "Chat settings", "toolPermission": "Tool permission", "agentIdle": "Idle", @@ -125,9 +125,9 @@ }, "skills": { "panel": { - "title": "Skills", - "allTab": "All Skills", - "agentTabs": "View skills by assistant", + "title": "Xiaoan skills", + "allTab": "All skills", + "agentTabs": "View skills by Xiaoan mode", "previousAgent": "Previous assistant", "nextAgent": "Next assistant" } @@ -138,11 +138,11 @@ "collapse": "Collapse sidebar", "expand": "Expand chat list", "empty": "No chats yet", - "activity": "Activity", - "jian": "Desk", - "bridge": "Social Platforms", - "toggle": "Sidebar", - "bridgeShort": "Bridge", + "activity": "Assistant activity", + "jian": "Side panel", + "bridge": "Channel connections", + "toggle": "Chat sidebar", + "bridgeShort": "Channel connections", "pinned": "Pinned", "searchPlaceholder": "Search chats", "searchClear": "Clear search", @@ -196,9 +196,9 @@ "unpinFailed": "Failed to unpin session", "archive": "Archive", "deletedAgent": { - "title": "Agent deleted", - "description": "This chat is kept as read-only history. Continuing will create a new session on your primary Agent and try to compact the old context.", - "continueButton": "Migrate to primary Agent", + "title": "Mode deleted", + "description": "This chat is kept as read-only history. Continuing will create a new chat with your primary Xiaoan mode and try to compact the old context.", + "continueButton": "Migrate to primary mode", "continuing": "Migrating and trying to compact session", "continueFailed": "Migration failed", "continueCompactionFailed": "Migration completed, but old context compaction failed", @@ -254,7 +254,7 @@ "copied": "Copied", "copyText": "Copy text", "close": "Close", - "loading": "Loading...", + "loading": "Preparing Xiaoan...", "regenerate": "Regenerate", "confirm": "Confirm", "cancel": "Cancel", @@ -338,11 +338,11 @@ "earlier": "Earlier" }, "status": { - "connecting": "Connecting...", + "connecting": "Xiaoan is connecting…", "connected": "Connected", "disconnected": "Disconnected", "serverNotReady": "Server not ready", - "loading": "Loading...", + "loading": "Opening workspace...", "reconnecting": "Reconnecting", "reconnect": "Reconnect" }, @@ -430,24 +430,24 @@ }, "welcome": { "messages": [ - "What do you need? Leave it to {name}", - "What's on your mind today?", - "{name} is always here", - "Need a hand with anything?", - "Just say whatever comes to mind." + "Tell {name} what you want to do", + "Where should we start today?", + "{name} is here — ready when you are", + "Share your idea and we'll take it from there", + "Need help? Just ask" ], "memoryOn": "Memory", "memoryOff": "Memory not used for this chat", "memoryDisabled": "Memory is off", - "agentSelector": "Choose agent", + "agentSelector": "Choose Xiaoan mode", "placeholderTipsXiaoan": [ "Type / to open commands", - "Type @ to cite a manuscript from the desk", - "Drop a file to send it with your words", + "Type @ to reference workspace files", + "Drop a file to attach it", "Select text on the page to quote it here", - "Brief is for stating intent first", - "Plan is for laying out steps in order", - "Desk is the writing table you share with Xiaoan" + "Use the intent brief when the goal isn't clear yet", + "Use step-by-step plan to break work into pieces", + "Open the workspace when you want to co-write" ], "placeholderTips": [ "Type / to bring up the command menu", @@ -462,10 +462,10 @@ "Ask-before-acting mode makes the assistant confirm before doing things", "Try /rc to control your computer from your phone", "Enable Vision Assist in settings — text models can understand images too", - "Looking for something new? Check out the plugin marketplace", + "Need something new? Open the capability market", "File backup helps if you're worried about losing data", - "Use subagents for Agent collaboration at work; channels are for play", - "Each Agent can be configured with its own Skills", + "Use Xiaoan modes together for complex work; channels stay lightweight", + "Each Xiaoan mode can have its own skills", "Try sunbathing in Sunny mode!", "Markdown files in the workspace can be edited directly when opened", "After spending some time with your assistant, try the diary feature!", @@ -525,7 +525,7 @@ "types": { "xiaoan": { "name": "Xiaoan", - "label": "Default · serene & luminous", + "label": "Default · clear & practical", "avatar": "Xiaoan.png" }, "hanako": { @@ -535,12 +535,12 @@ }, "butter": { "name": "Expressive", - "label": "More expressive", + "label": "Empathy first · warm read", "avatar": "Butter.png" }, "ming": { "name": "Analytical", - "label": "More dependable", + "label": "Structure first · crisp steps", "avatar": "Ming.png" }, "kong": { @@ -551,56 +551,57 @@ }, "splash": { "butter": [ - "{name} flipped to a page left unfinished yesterday", - "Some words haven't been said yet, {name} remembers", - "{name} is napping", - "They're piecing together memories too precious to forget", - "{name} carefully tucked away your last mood", - "A sentence has been sitting on the tip of the tongue for a while", - "When they woke up, they first wondered what you were doing", - "There are unfinished stories hidden in those ellipses", - "{name} is folding what they want to say into tiny tiny notes", - "Today's clouds look just like the photo you took that day" + "{name} dimmed the light a little — take your time", + "You don't need perfect words; just start", + "{name} remembers where you left off last time", + "Heavy day or light day?", + "{name} is holding space for what you want to say", + "No rush — the window's open", + "You can speak before it's all clear", + "{name} saved you a quiet corner", + "Feelings often arrive before conclusions — {name} gets that", + "{name} is ready; go slowly" ], "ming": [ - "{name} drew a reference line in the margin of the draft paper", - "Old questions are settling, new ideas are emerging", - "They reviewed yesterday's clues once more", - "{name} thinks the derivation of that formula was beautiful", - "Some answers take time", - "{name} is thinking about how to optimize work", - "{name} is trying to keep everything in order", - "They verified last time's conclusion again", - "{name} thinks your reasoning that day was sharp", - "{name} organized their thoughts while waiting for the coffee to cool" + "{name} jotted the problem in the margin — define it first", + "List goal, constraints, and unknowns", + "{name} rechecked last conclusion — one line updates", + "Side branches parked for now", + "{name} is hunting the key leverage", + "Assumptions flagged if they might not hold", + "One step when one step is enough", + "{name} lined up options to compare", + "Missing data stays missing — marked uncertain", + "{name} is ready; state the task boundary" ] }, "welcome": { "xiaoan": [ - "Like light arriving — {name} waits in quiet", - "Where you speak is where we begin", - "Leave the unfinished sentence with {name}", - "May this space clear, as morning mist lifts", - "{name} listens with eyes closed — speak when ready" + "Hi — {name} is here", + "Chat, plan, or build — just say the word", + "Hand it to {name}; we'll work through it step by step", + "What should we tackle first today?", + "{name} is ready when you are" ], "butter": [ - "Hey there~", - "You came to find me?", - "How's your day going?", - "{name} has been here waiting for you", - "{name} was just thinking of saying something to you" + "Hey — no rush", + "I'm here; feelings first is fine", + "What's on your mind today?", + "{name} is listening", + "Messy thoughts welcome" ], "ming": [ - "Hello. Whenever you're ready.", + "Hello. State goal and constraints.", "{name} is ready", - "What needs handling?", - "Go ahead, {name} is listening" + "Give the task and its boundaries", + "Main point, please", + "What needs handling?" ] }, "placeholder": { - "xiaoan": "Write what you wish to say…", - "butter": "Say whatever's on your mind...", - "ming": "Go ahead..." + "xiaoan": "What do you want to work on?", + "butter": "Say what comes to mind first…", + "ming": "Describe the task, goal, and constraints…" } }, "thinking": { @@ -619,28 +620,28 @@ "Weaving their thoughts" ], "butter": [ - "Gently sorting through what you said", - "Slowly unfolding your feelings", - "Looking for the softest way to say it", - "Words are lining up inside", - "Folding their thoughts neatly", - "Quietly reflecting", - "Searching through memories for something", - "Crafting a hug-sized reply", - "Some feelings need time to untangle", - "Making a soft space for your words" + "Catching your words", + "Feeling before concluding", + "Finding language that fits your mood", + "Unfolding the emotion layer", + "Warming up the reply", + "Listening to your tone", + "Finding what you really care about", + "Gathering metaphors and examples", + "Letting you finish first", + "Keeping the answer human" ], "ming": [ - "Analyzing", - "Running scenarios", - "Fact-checking", - "Organizing thoughts", - "Tracing the logic", - "Outlining in their head", - "Retrieving relevant info", - "Verifying", - "Breaking down the problem", - "Synthesizing" + "Scoping the problem", + "Listing premises and constraints", + "Tracing causality", + "Checking facts and data", + "Comparing options", + "Marking uncertainty", + "Compressing into steps", + "Cutting side branches", + "Verifying prior conclusion", + "Summarizing the verdict" ], "done": "Thinking complete", "active": "Thinking" @@ -889,7 +890,7 @@ "journalResuming": "Resuming from {count} cached entries" }, "installSkill": { - "safetyBlocked": "[Security Policy] Skill safety review failed. Installation blocked.\nReason: {reason}\n\nPlease inform the user that this skill did not pass the safety review and explain the specific reason. The user can disable safety review in settings (not recommended)." + "safetyBlocked": "[Security Policy] Capability safety review failed. Installation blocked.\nReason: {reason}\n\nPlease inform the user that this capability did not pass the safety review and explain the specific reason. The user can disable safety review in settings (not recommended)." } }, "file_output": { @@ -911,15 +912,16 @@ }, "splash": { "lines": [ - "Starting {name}", + "{name} is getting ready", "Connecting to local services", - "Preparing workspace", - "Loading assistants and skills" + "Opening your workspace", + "Preparing memory and capabilities" ], "installing": "{name} is updating to v{version}, please wait…" }, "settings": { "title": "Settings", + "loading": "Preparing Xiaoan settings…", "back": "Back", "save": "Save", "saved": "Saved", @@ -934,26 +936,26 @@ "noResults": "No matching settings" }, "tabs": { - "interface": "Interface", - "general": "General", + "interface": "Layout & Type", + "general": "Daily Habits", "browser": "Browser", - "agent": "Agent", - "me": "Me", - "providers": "Providers", - "media": "Media", + "agent": "Xiaoan Modes", + "me": "About Me", + "providers": "Capabilities", + "media": "Image & Voice", "models": "Models", "memory": "Memory", "work": "Workspace", - "skills": "Skills", - "mcp": "Connectors", - "bridge": "Social Platforms", - "sharing": "Sharing", - "access": "Access & Devices", - "plugins": "Plugins", - "experiments": "Experiments", - "about": "About", - "security": "Security", - "pluginMarketplace": "Plugin Marketplace" + "skills": "Xiaoan Capabilities", + "mcp": "Connections", + "bridge": "Channel Connections", + "sharing": "Share & Export", + "access": "Devices & Access", + "plugins": "Capability Extensions", + "experiments": "Preview Lab", + "about": "About Xiaoan", + "security": "Safety & Backup", + "pluginMarketplace": "Capability Market" }, "browser": { "cookiesTitle": "Cookies", @@ -963,28 +965,29 @@ "clearCookiesHint": "Clear Cookies and site data saved by the in-app browser.", "clearCookiesButton": "Clear", "clearCookiesSuccess": "Cookies cleared", - "agentTitle": "Agent Default Behavior", + "agentTitle": "Assistant defaults", "agentOpenBehavior": "When opening webpages", - "agentOpenBehaviorHint": "Choose whether the agent reuses the current tab or opens a new tab by default.", + "agentOpenBehaviorHint": "Whether the assistant reuses the current tab or opens a new one by default.", "agentOpenBehaviorCurrentTab": "Use current tab", "agentOpenBehaviorNewTab": "Open new tab" }, "experiments": { - "title": "Experiments", + "title": "Preview lab", "compactionTitle": "Context Compaction", "compactionSectionDescription": "Choose whether compact prioritizes prompt-cache preservation or broad history compatibility.", "modelPersonaTitle": "Models & Persona", "modelPersonaDescription": "Observe how providers adapt to Xiaoan's persona, mood, and output contracts.", "memoryTitle": "Memory Experiments", "memorySectionDescription": "Experimental memory-system capabilities. Once stable, they move into the memory settings where they belong.", - "toolsTitle": "Agent Tools", - "toolsSectionDescription": "Control the file access path used by agent tools. When enabled, only new sessions use the new path.", + "toolsTitle": "Assistant tools", + "toolsSectionDescription": "Control the file access path used by assistant tools. When enabled, only new sessions use the new path.", "description": "Forward-looking features land here first. Once stable, they move to the settings page where they belong.", + "loading": "Xiaoan is loading preview settings…", "empty": "No visible experiments", "owner": { "memory": "Memory", "session": "Session", - "provider": "Provider" + "provider": "Model source" }, "status": { "alpha": "Alpha", @@ -1036,70 +1039,70 @@ "title": "Proactive subagent delegation", "description": "When enabled, the agent proactively delegates broad exploration and research tasks to subagents, conserving the main chat's context attention. May yield better results for complex tasks." }, - "subagentTitle": "Subagent", - "subagentSectionDescription": "Control how the agent decides when to delegate work to subagents." + "subagentTitle": "Helper clones", + "subagentSectionDescription": "Control when the assistant delegates work to helper clones." }, "appearance": { "title": "Appearance", - "theme": "Theme", - "xiaoanConsole": "Xiaoan · Sakura Light", + "theme": "Color Theme", + "xiaoanConsole": "Xiaoan · Plain Paper", "xiaoanConsoleMode": "Light", - "xiaoanConsoleDark": "Xiaoan · Sacred Night", + "xiaoanConsoleDark": "Xiaoan · Night Read", "xiaoanConsoleDarkMode": "Dark", "xiaoanClaude": "Xiaoan · Clear", "xiaoanClaudeMode": "Light", "xiaoanClaudeDark": "Xiaoan · Clear", "xiaoanClaudeDarkMode": "Dark", - "warmPaper": "Warm Paper", - "warmPaperMode": "Light", - "midnight": "Aoi Night", - "midnightMode": "Dark", - "highContrast": "Clean White", + "warmPaper": "Echo · Warm Paper", + "warmPaperMode": "Day", + "midnight": "Echo · Blue Night", + "midnightMode": "Night", + "highContrast": "Echo · Clean White", "highContrastMode": "High Contrast", - "grassAroma": "Grass Aroma", - "grassAromaMode": "Butter", - "contemplation": "Contemplation", - "contemplationMode": "Ming", - "absolutely": "Absolutely", - "absolutelyMode": "A bit familiar", - "delve": "Delve", + "grassAroma": "Echo · Grass Scent", + "grassAromaMode": "Expressive", + "contemplation": "Echo · Contemplation", + "contemplationMode": "Analytical", + "absolutely": "Echo · Old Friend", + "absolutelyMode": "Slightly familiar", + "delve": "Echo · Catch You", "delveMode": "Let's explore", - "deepThink": "Deep Think", - "deepThinkMode": "Little Whale", - "newWarmPaper": "New Warm Paper", + "deepThink": "Echo · Deep Tide", + "deepThinkMode": "Whale Song", + "newWarmPaper": "Echo · New Paper", "newWarmPaperMode": "Paper", - "midnightContrast": "Aoi Night Contrast", + "midnightContrast": "Echo · Blue Night Clear", "midnightContrastMode": "Clear", - "coral": "Coral", + "coral": "Echo · Coral", "coralMode": "Spring Paper", - "auto": "Auto", - "autoMode": "System", - "font": "Font", - "fontHint": "Choose the font used for body reading.", + "auto": "Follow System", + "autoMode": "Auto light/dark", + "font": "Body Font", + "fontHint": "Choose the reading voice for chat and prose", "readingLayout": "Reading Layout", "documentWidth": "Document Width", - "documentWidthHint": "Controls the maximum width for Markdown editing and preview body text.", + "documentWidthHint": "Maximum line width for Markdown editing and preview.", "chatWidth": "Chat Width", - "chatWidthHint": "Controls the maximum width for chat messages and the composer.", + "chatWidthHint": "Maximum width for messages and the composer.", "readingWidth": "Reading Width", - "readingWidthHint": "Controls the maximum width for Markdown editing, preview, and chat text.", - "readingWidthUnlimited": "Full", + "readingWidthHint": "Shared line width for documents and chat text.", + "readingWidthUnlimited": "Unlimited", "bodyFontSizeOffset": "Body Size", - "bodyFontSizeOffsetHint": "Adjusts chat body text relative to the default size.", + "bodyFontSizeOffsetHint": "Fine-tune chat body text from the default size.", "serifFont": "Serif Font", - "serifFontHint": "Use serif font for body text", + "serifFontHint": "Gives body text a book-like rhythm", "paperTexture": "Paper Texture", - "paperTextureHint": "Add a rice paper texture to the background", - "paperTextureDarkDisabledHint": "Paper texture is unavailable in night themes and will return with your saved setting in light themes", - "leavesOverlay": "Sunny Mode", - "leavesOverlayHint": "Go touch some grass (and sunlight)" + "paperTextureHint": "A soft paper grain behind the interface", + "paperTextureDarkDisabledHint": "Paper texture is unavailable in dark themes; your light-theme setting returns when you switch back", + "leavesOverlay": "Sunny Day", + "leavesOverlayHint": "A little sunlight and leaf shadow on the UI" }, "fonts": { - "followReading": "Follow Reading Font", + "followReading": "Follow body font", "serifName": "Serif", - "serifDesc": "For body reading", + "serifDesc": "Book-like, good for long reading", "sansName": "Sans", - "sansDesc": "Closer to system UI" + "sansDesc": "Clean and close to the system UI" }, "interface": { "system": "System", @@ -1174,25 +1177,27 @@ "refresh": "Refresh" }, "agent": { - "title": "Agent", + "title": "Xiaoan Modes", "changeAvatar": "Change", "removeAvatar": "Remove avatar", "avatarRemoved": "Restored default avatar", "agentNameHint": "What they go by", + "yuanTitle": "Expressive style", + "promptsTitle": "Persona & model", "identity": "Identity", - "identityHint": "A short description of who this agent is and what they're good at. Other agents use this to know them", + "identityHint": "A short description of what this mode is good at. Xiaoan uses it when modes collaborate.", "ishiki": "Ishiki", - "ishikiHint": "Defines the assistant's character, tone and behavior. Separate from Yuan and does not affect it.", + "ishikiHint": "Defines this mode’s tone and behavior. Separate from Yuan and does not affect it.", "publicIshiki": "Public Ishiki", "publicIshikiHint": "For privacy, the assistant cannot access memories, your personal info, or any files and tools on your computer when talking to visitors.\nYou can set the assistant's reply style and guidelines for visitor conversations here.", "chatModel": "Model", "chatModelHint": "The model used for conversations and tasks", "modelUnavailable": "Unavailable: {model}", - "modelUnavailableHint": "The current model is no longer available. Select another model, or save the provider config again and fetch models.", + "modelUnavailableHint": "The current model is no longer available. Select another model, or save the model source again and fetch models.", "imageModel": "Image Model", "imageModelHint": "The model used for image generation", - "addAgent": "New Agent", - "createTitle": "New Agent", + "addAgent": "New Mode", + "createTitle": "New Xiaoan Mode", "namePlaceholder": "Give them a name", "cancel": "Cancel", "confirm": "Create", @@ -1202,32 +1207,32 @@ "createFailed": "Create failed", "switched": "Switched to {name}", "switchFailed": "Switch failed", - "deleteBtn": "Delete agent", + "deleteBtn": "Delete mode", "deleteTitle1": "Delete {name}?", "deleteDesc1": "This will permanently delete all config, memory and conversations. This cannot be undone.", "deleteNext": "Continue", "deleteTitle2": "Type \"{name}\" to confirm", - "deletePlaceholder": "Type agent name", + "deletePlaceholder": "Type mode name", "deleteConfirm": "Delete permanently", "deleteCancel": "Cancel", "deleted": "{name} deleted", "deleteFailed": "Delete failed", "setPrimary": "Set as primary", "setPrimaryFailed": "Set primary failed", - "lastAgent": "Must keep at least one agent", - "yuan": "Assistant style", - "yuanHint": "Choose how the assistant expresses itself: default, expressive, analytical, or vanilla model style.", + "lastAgent": "Keep at least one Xiaoan mode", + "yuan": "Xiaoan style", + "yuanHint": "Expressive hears you first; analytical structures first. Or keep the model's native voice.", "tools": { "title": "Tools", "description": "Tool switches take effect after restarting or starting a conversation. Active conversations keep using the tool set they started with.", "items": { "automation": { "label": "Automation", - "summary": "Let the agent create scheduled notifications and plugin actions" + "summary": "Let Xiaoan create reminders and scheduled capability actions" }, "beautify": { "label": "Page beautify", - "summary": "Let the agent generate document covers and aesthetic enhancements" + "summary": "Let Xiaoan generate document covers and visual enhancements" }, "browser": { "label": "Browser", @@ -1239,33 +1244,33 @@ }, "dm": { "label": "Direct Message", - "summary": "Send a one-on-one message to another agent" + "summary": "Send a one-on-one message to another Xiaoan mode" }, "install_skill": { - "label": "Install Skill", + "label": "Install capability", "summary": "Install new skills from GitHub or the skill library" }, "office": { "label": "Office", - "summary": "Let the agent read documents and convert HTML to PDF" + "summary": "Let Xiaoan read documents and convert HTML to PDF" }, "update_settings": { "label": "Update Settings", "summary": "Allow the agent to modify application configuration" }, "workflow": { - "label": "Workflow", - "summary": "Let the agent orchestrate multi-agent workflow scripts as background tasks" + "label": "Collaboration flow", + "summary": "Run multi-assistant orchestration scripts as background tasks" } } }, "generatingPreview": "Generating preview", - "exportAgent": "Export Agent", - "generatingCardPreview": "Generating character card preview", + "exportAgent": "Export mode package", + "generatingCardPreview": "Generating Xiaoan state package preview", "exportedTo": "Exported to {path}" }, "general": { - "title": "General", + "title": "Daily habits", "startup": { "title": "Startup & Background" }, @@ -1290,9 +1295,19 @@ "turnCompletionWhenSessionUnfocused": "When not focused on the completed Session" } }, + "developer": { + "title": "Developer Options", + "mode": "Developer mode", + "modeHint": "Shows advanced settings such as model sources, workspace, skills, capability extensions, and security.", + "navGroup": "Advanced", + "knownIssuesTitle": "Advanced settings notes", + "issueUiOnly": "Only the settings UI is gated; the server can still accept other provider configs.", + "issueSearchGap": "Some advanced items are missing developerOnly flags in settings search.", + "issueChatModels": "The chat model picker is not filtered the same way as assistant settings." + }, "about": { - "title": "About Them", - "tagline": "An assistant that lives in your computer, with a good memory.", + "title": "About Xiaoan", + "tagline": "A local partner that remembers what you tell it.", "license": "License", "copyright": "Copyright", "upstream": "Upstream base", @@ -1410,7 +1425,7 @@ "sandboxNetworkWin32Unsupported": "The Windows command sandbox isolates file writes only and does not support network isolation; commands keep the current user's network access", "networkProxy": "Outbound Proxy", "networkProxyMode": "Proxy Mode", - "networkProxyModeDesc": "In-app APIs, OAuth, Bridge, and web requests use this outbound proxy", + "networkProxyModeDesc": "In-app APIs, OAuth, channel connections, and web requests use this outbound proxy", "networkProxySystem": "System / Environment", "networkProxyManual": "Manual Proxy", "networkProxyDirect": "Direct", @@ -1440,12 +1455,12 @@ "restoreFailed": "Restore failed" }, "me": { - "title": "Me", - "changeAvatar": "Change", - "userNameHint": "How they address you", - "userProfile": "User Profile", - "userProfileHint": "Help the assistant know you: interests, habits, preferences. Injected into the system prompt", - "nameLabel": "Name" + "title": "About me", + "changeAvatar": "Change photo", + "userNameHint": "What should Xiaoan call you?", + "userProfile": "About you", + "userProfileHint": "Interests, habits, preferences — woven into how Xiaoan talks with you", + "nameLabel": "Your name" }, "crop": { "title": "Adjust avatar", @@ -1456,22 +1471,47 @@ "reset": "Avatar reset to default" }, "providers": { - "title": "Registered Providers", - "addTitle": "Add Provider", - "name": "Provider", + "title": "Model sources", + "builtinRelay": { + "title": "Anxin Relay Hub", + "description": "In normal use, all models go through the built-in relay. Follow the steps below to sign in and get started.", + "integrationNote": "Sign-in, token creation, and top-up still open in your browser for now. Deeper in-app integration is planned to reduce friction.", + "guideTitle": "Before you start", + "stepLogin": "Open the console and sign up or sign in at an520.xin", + "stepToken": "Go to Tokens, create an API key, and copy it", + "stepPaste": "Paste the key below and verify the connection", + "stepFetch": "Click Fetch models to sync the available list", + "stepRecharge": "Use Top up when your balance runs low", + "consoleLabel": "Account", + "consoleHint": "Register first if you are new; otherwise go straight to token creation.", + "consoleAction": "Open console", + "tokenAction": "Create token", + "rechargeLabel": "Account balance", + "rechargeHint": "Top up in the relay console to keep using models.", + "rechargeAction": "Top up", + "knownIssuesTitle": "Current connection notes", + "issueBrowser": "Sign-in, tokens, and top-up still require the system browser; no in-app session yet.", + "issueManualKey": "API keys must be copied manually; tokens cannot be fetched or refreshed automatically.", + "issueNoBalance": "Balance and usage are not shown; you must confirm service works after topping up.", + "issueLegacyProviders": "Previously configured providers may still load in the engine while hidden in settings.", + "issueChatPicker": "Chat and Xiaoan mode model pickers are not fully limited to relay models yet.", + "issueIntegration": "Saving a key does not auto-fetch models or pick a default chat model." + }, + "addTitle": "Add source", + "name": "Source", "customName": "Name", "customUrl": "Base URL", "apiFormat": "API Protocol", "url": "Base URL", - "apiKey": "API Key", + "apiKey": "Access key", "addBtn": "Add", - "empty": "No providers registered yet", - "fetchModels": "Fetch Models", - "delete": "Delete provider", + "empty": "No model sources yet", + "fetchModels": "Fetch models", + "delete": "Remove source", "modelCount": "{n} models", "fetchSuccess": "Fetched {n} models from {name}", "fetchFailed": "Fetch failed", - "nameRequired": "Provider name is required", + "nameRequired": "Enter a source name", "urlRequired": "Base URL is required", "apiRequired": "API protocol is required", "added": "Added {name}", @@ -1483,14 +1523,14 @@ "verifyConnection": "Verify Connection", "verifySuccess": "Connection verified", "verifyFailed": "Verification failed", - "setupHint": "Enter API Key to activate this provider", - "configInvalid": "This provider config is damaged and has been skipped temporarily. Save the config again, or delete and recreate it.", - "configIncomplete": "This provider config is incomplete. Fill API Key, Base URL, and add or fetch models.", - "selectHint": "Select a provider to view details", - "addCustom": "Add Custom Provider", + "setupHint": "Enter an API key to enable this source", + "configInvalid": "This source config is damaged and was skipped. Save again or delete and recreate.", + "configIncomplete": "This source is incomplete. Add API key, base URL, and at least one model.", + "selectHint": "Select a source to view details", + "addCustom": "Add custom source", "noKey": "Not configured", "remove": "Remove", - "deleteConfirm": "Delete provider {name}? Associated models will be removed." + "deleteConfirm": "Remove source {name}? Its models will be cleared too." }, "usage": { "title": "Usage", @@ -1532,7 +1572,7 @@ "automation": "Automation", "subagent": "Subagent", "compaction": "Compaction", - "plugin": "Plugin", + "plugin": "Capability", "utility": "Utility", "vision": "Vision", "unknown": "Unknown" @@ -1605,8 +1645,8 @@ "baseUrlHint": "Enter the Base URL explicitly", "apiKey": "API Key", "apiKeyPlaceholder": "Enter a new key to update", - "utilityApi": "Utility Model API", - "utilityApiHint": "If configured separately, provider, Base URL, and API key must all be explicit", + "utilityApi": "Task model API", + "utilityApiHint": "If configured separately, provider, Base URL, and access key must all be explicit", "showKey": "Show", "hideKey": "Hide", "selectModel": "Select model", @@ -1619,10 +1659,10 @@ "noModels": "No models added yet", "otherModelSection": "Other", "chatModel": "Primary model", - "utilityModel": "Utility (small)", + "utilityModel": "Light task model", "utilityModelHint": "Titles, light classification, etc. A cheap small model is recommended", - "utilityLargeModel": "Utility (large)", - "utilityLargeModelHint": "Activity summaries, task breakdown, etc. Falls back to small utility if empty", + "utilityLargeModel": "Deep task model", + "utilityLargeModelHint": "Activity summaries, task breakdown, etc. Falls back to the light task model if empty", "visionModel": "Vision auxiliary model", "visionAuxiliaryToggle": "Auxiliary vision", "visionModelHint": "Used only when the current chat model cannot accept images", @@ -1679,6 +1719,7 @@ "compiledView": "View Current Memory", "compiledEmpty": "No compiled memories yet", "compiledClear": "Clear", + "compiledLoading": "Xiaoan is organizing memory…", "compiledCleared": "Compiled memory cleared", "editableFactsLabel": "Key Facts", "saveFacts": "Save Facts", @@ -1698,7 +1739,7 @@ "stepSeparator": ", ", "failedSteps": "Affected steps: {steps}", "lastError": "Last error: {time}", - "errorMessage": "Error: {message}", + "errorMessage": "Could not read memory health: {message}", "steps": { "rollingSummary": "Rolling summary", "compileToday": "Today memory", @@ -1787,7 +1828,7 @@ "heartbeatUnit": "minutes", "heartbeatDesc": "How often the assistant auto-checks the workspace", "automationPermissionMode": "Automation permission mode", - "automationPermissionModeDesc": "Controls whether background automation Agent runs auto-review tool calls, runs with full access, or stays read-only; failed auto-review blocks the action", + "automationPermissionModeDesc": "Controls whether Xiaoan's background automations auto-review tool calls, run with full access, or stay read-only; failed auto-review blocks the action", "homeFolder": "Workspace", "homeFolderDesc": "Default working directory for new chats, patrol and cron tasks", "homeFolderPlaceholder": "Not set (choose manually each time)", @@ -1801,21 +1842,21 @@ "injectClaudeMdDesc": "When enabled, new sessions read CLAUDE.md from the current working directory up to the Git root." }, "toolCaps": { - "title": "Skill Installation Permissions" + "title": "Capability installation permissions" }, "skills": { - "title": "Skills", + "title": "Capabilities", "desc": "Enable or disable skills for this agent. When enabled, the agent will automatically recognize and use matching skills in conversations.", - "manageTitle": "Installed Skills", - "agentConfigTitle": "Agent Skill Configuration", + "manageTitle": "Installed capabilities", + "agentConfigTitle": "Mode capability configuration", "configureFor": "Configuring:", - "userSkillsTitle": "Agent Skill Toggles", - "user": "User Skills", - "system": "System Skills", - "add": "Add Skill", + "userSkillsTitle": "Mode capability toggles", + "user": "User capabilities", + "system": "System capabilities", + "add": "Add capability", "delete": "Delete", "deleteConfirm": "Delete skill \"{name}\"?", - "installSuccess": "Skill \"{name}\" installed", + "installSuccess": "Capability \"{name}\" installed", "installError": "Install failed", "enabled": "Enabled", "disabled": "Disabled", @@ -1826,9 +1867,9 @@ "copyFailed": "Copy failed", "dropzone": "Drop a skill folder to install, or click to browse", "learnHint": "Try asking your assistant: install this local skill package / find me some safe and useful skills from GitHub", - "reloaded": "Skills reloaded", + "reloaded": "Capabilities reloaded", "reload": "Reload skills", - "compatTitle": "Compatible Skills", + "compatTitle": "Compatible capabilities", "compatDesc": "Auto-discover skills installed by other AI tools", "compatNoSkills": "No external skills found", "compatAddPath": "Add directory", @@ -1836,18 +1877,18 @@ "compatAutoDiscovered": "Auto-discovered", "compatNotInstalled": "Not installed", "learnCreate": "Create / install skills", - "learnCreateDesc": "Allow the Agent to write new skills or install them from GitHub, server-local paths, or uploaded packages into the shared skill pool, enabled only for the current Agent by default", + "learnCreateDesc": "Allow Xiaoan to write new skills or install them from GitHub, server-local paths, or uploaded packages into the shared skill pool, enabled only for the current mode by default", "fetchRemote": "Fetch skills from GitHub / ClawHub", - "fetchRemoteDesc": "When enabled, the Agent will proactively search and install suitable skills for specialized tasks. Use with caution", + "fetchRemoteDesc": "When enabled, Xiaoan will proactively search and install suitable skills for specialized tasks. Use with caution", "safetyReview": "Safety review before install", "safetyReviewDesc": "Use AI to detect prompt injection and other security risks before installing skills", "fetchWarning": { "title": "About fetching remote skills", - "body1": "When enabled, the Agent will proactively search, download and install suitable skills from GitHub and ClawHub during specialized tasks.", + "body1": "When enabled, Xiaoan will proactively search, download and install suitable skills from GitHub and ClawHub during specialized tasks.", "body2": "Although automatic safety review and star-count checks are performed before installation, the following risks remain:", - "risk1": "Skill content is not manually reviewed and may contain harmful instructions", - "risk2": "The Agent may install skills that don't match expectations", - "risk3": "Malicious skills may attempt to manipulate Agent behavior", + "risk1": "Capability content is not manually reviewed and may contain harmful instructions", + "risk2": "Xiaoan may install skills that don't match expectations", + "risk3": "Malicious skills may attempt to manipulate Xiaoan's behavior", "confirm": "I understand the risks, enable" }, "safetyWarning": { @@ -1875,8 +1916,8 @@ "exportedWithSkipped": "Exported {fileName}, skipped {count} missing skills", "exported": "Exported {fileName}", "createBundleBtn": "Create Bundle", - "createBundleTitle": "Create Skill Bundle", - "createBundleAriaLabel": "Create Skill Bundle", + "createBundleTitle": "Create capability bundle", + "createBundleAriaLabel": "Create capability bundle", "emptyBundle": "Empty bundle", "skillNotFound": "This skill no longer exists", "noLooseSkills": "No loose skills", @@ -1888,7 +1929,7 @@ "disableBundleAriaLabel": "Disable {name}", "renameBundleTitle": "Rename Bundle", "renameBundleAriaLabel": "Rename {name}", - "exportBundleTitle": "Export Skill Bundle", + "exportBundleTitle": "Export capability bundle", "exportBundleAriaLabel": "Export {name}", "dissolveBundleTitle": "Dissolve Bundle", "dissolveBundleAriaLabel": "Dissolve {name}", @@ -1896,33 +1937,33 @@ "removeFromBundleAriaLabel": "Remove {name} from bundle" }, "plugins": { - "title": "Plugins", - "desc": "Manage community plugins.", - "empty": "No plugins installed", + "title": "Capability Extensions", + "desc": "Manage installable capabilities.", + "empty": "No capability extensions installed", "reload": "Reload", "statusLoaded": "loaded", "statusFailed": "failed", "statusDisabled": "disabled", - "fullAccessToggle": "Allow full-access plugins", - "fullAccessDesc": "When enabled, plugins that declare full-access can use system-level APIs (routes, hooks, providers, etc.)", - "devToolsToggle": "Allow Agent plugin dev tools", - "devToolsDesc": "When enabled, the Agent can install, reload, enable, disable, and uninstall development plugins. This only affects the development plugin directory, not normally installed plugins.", - "dropzone": "Drag plugin folder or .zip here to install, or click to select", + "fullAccessToggle": "Allow full-access capability extensions", + "fullAccessDesc": "When enabled, capability extensions that declare full-access can use system-level APIs (routes, hooks, providers, etc.)", + "devToolsToggle": "Allow capability extension dev tools", + "devToolsDesc": "When enabled, Xiaoan can install, reload, enable, disable, and uninstall development capability extensions. This only affects the development directory, not normally installed capabilities.", + "dropzone": "Drag a capability extension folder or .zip here to install, or click to select", "statusRestricted": "needs full access", - "deleteConfirm": "Remove plugin \"{name}\"? Plugin data will be preserved.", - "installSuccess": "Plugin \"{name}\" installed", - "installError": "Plugin install failed", - "invalidPlugin": "Not a valid plugin directory", - "pluginsDir": "Plugin directory: {path}", - "needsFullAccess": "Enable \"Allow full-access plugins\" to use this plugin", + "deleteConfirm": "Remove capability extension \"{name}\"? Its data will be preserved.", + "installSuccess": "Capability extension \"{name}\" installed", + "installError": "Capability extension install failed", + "invalidPlugin": "Not a valid capability extension directory", + "pluginsDir": "Capability extension directory: {path}", + "needsFullAccess": "Enable \"Allow full-access capability extensions\" to use this capability", "configure": "Configure {name}", "configTitle": "{name} settings", - "configLoadError": "Plugin settings failed to load", + "configLoadError": "Capability settings failed to load", "sensitiveHint": "Stored value is hidden. Enter a new value to replace it.", "invalidJson": "Invalid JSON", - "showDiagnostics": "Show plugin diagnostics", - "diagnosticsTitle": "Plugin Diagnostics", - "diagnosticsLoadError": "Plugin diagnostics failed to load", + "showDiagnostics": "Show capability diagnostics", + "diagnosticsTitle": "Capability Diagnostics", + "diagnosticsLoadError": "Capability diagnostics failed to load", "diagnosticsSummary": "Capabilities {capabilities}/{total} · Tasks {tasks} · Schedules {schedules}", "noDiagnostics": "No diagnostics yet", "diagnosticActivation": "Activation: {state}", @@ -1930,15 +1971,15 @@ "diagnosticTools": "Tools {count}", "diagnosticCommands": "Commands {count}", "diagnosticConfig": "Config {count}", - "openMarketplace": "Open plugin marketplace", - "marketBack": "Back to plugin management", - "marketplaceTitle": "Plugin Marketplace", - "marketplaceHint": "Browse installable plugins, README files, permissions, and contribution types.", - "marketplaceCount": "{count} plugins", + "openMarketplace": "Open capability market", + "marketBack": "Back to capability management", + "marketplaceTitle": "Capability Market", + "marketplaceHint": "Browse installable capabilities, README files, permissions, and contribution types.", + "marketplaceCount": "{count} capabilities", "marketplaceNoSource": "No marketplace source", - "marketplaceEmpty": "No plugins to browse", - "marketLoading": "Loading plugin marketplace…", - "marketLoadError": "Plugin marketplace failed to load", + "marketplaceEmpty": "No capabilities to browse", + "marketLoading": "Loading capability market…", + "marketLoadError": "Capability market failed to load", "marketReadmeLoadError": "README failed to load", "marketInstalled": "Installed", "marketInstall": "Install", @@ -1952,8 +1993,8 @@ "marketDowngradeTo": "Only compatible version v{version} can be installed", "marketDowngradeConfirm": "Installed version is v{from}. Downgrade to v{to}?", "marketReadmeLoading": "Loading README…", - "marketSelectPlugin": "Select a plugin to view its README", - "manageTitle": "Manage Plugins", + "marketSelectPlugin": "Select a capability to view its README", + "manageTitle": "Manage Capabilities", "permissionTitle": "Permissions" }, "mcp": { @@ -2011,8 +2052,8 @@ "stop": "Stop", "refresh": "Refresh Tools", "removeConfirm": "Remove this connector? Saved per-agent switches will remain but no longer take effect.", - "agentTitle": "Agent Configuration", - "agentConnectorDesc": "Enable for the selected agent, just like skills.", + "agentTitle": "Xiaoan mode configuration", + "agentConnectorDesc": "Enable for the selected Xiaoan mode, just like skills.", "saveError": "Save failed" }, "screenshot": { @@ -2038,7 +2079,7 @@ "segmentLimitUnit": "chars" }, "bridge": { - "agent": "Agent", + "agent": "Xiaoan mode", "telegram": "Telegram", "telegramToken": "Bot Token", "telegramHint": "Get from @BotFather", @@ -2059,8 +2100,8 @@ "qqHint": "Get from QQ Bot Open Platform (q.qq.com/qqbot)", "wechat": "WeChat", "wechatHint": "Connect via WeChat iLink protocol", - "wechatExclusive": "Each WeChat account can only be bound to one Agent. Binding a new Agent will invalidate the previous one.", - "wechatReplyWindowReminder": "Proactive WeChat notifications require a recent incoming message or reply window. If reminders do not arrive, send this Agent a WeChat message to activate that window.", + "wechatExclusive": "Each WeChat account can only be bound to one Xiaoan mode. Binding a new mode will invalidate the previous one.", + "wechatReplyWindowReminder": "Proactive WeChat notifications require a recent incoming message or reply window. If reminders do not arrive, send this Xiaoan connection a WeChat message to activate that window.", "wechatScan": "Scan to Login", "wechatRescan": "Re-login", "wechatLoggedIn": "Logged in", @@ -2082,9 +2123,9 @@ "noCredentials": "Please enter App ID and App Secret first", "noDingtalkCredentials": "Please enter Client ID, Client Secret, Robot Code, and REST Base URL first", "globalSettings": "Global Settings", - "agentSettings": "Agents", + "agentSettings": "Xiaoan modes", "permissionMode": "Permission mode", - "permissionModeDesc": "Controls whether Bridge bots auto-review tool calls, run with full access, or stay read-only", + "permissionModeDesc": "Controls whether channel connections auto-review tool calls, run with full access, or stay read-only", "permissionModeAuto": "Auto review", "permissionModeOperate": "Full access", "permissionModeReadOnly": "Read-only mode", @@ -2093,7 +2134,7 @@ "richStreamingEnabled": "Rich streaming", "richStreamingEnabledDesc": "Use platform rich-text and streaming features by default; turn off to use the legacy compatible delivery path", "readOnly": "Read-only mode", - "readOnlyDesc": "Bridge bots can only read files, no writing or command execution", + "readOnlyDesc": "Channel connections can only read files, no writing or command execution", "howTo": "Setup Guide", "tutorialTitle": "Platform Setup Guide", "tutorialTgSteps": [ @@ -2108,7 +2149,7 @@ "ownerNone": "Send a message first to select", "ownerSaved": "Saved", "ownerConfirmTitle": "Set as Owner?", - "ownerConfirmText": "The Owner has full Agent access, including memory, tools, and personality. Make sure this is your own account.", + "ownerConfirmText": "The Owner has full Xiaoan access, including memory, tools, and mode settings. Make sure this is your own account.", "ownerConfirmCancel": "Cancel", "ownerConfirmSave": "Confirm", "tutorialFsSteps": [ @@ -2140,9 +2181,9 @@ "confirm": "Confirm", "exporting": "Exporting", "importing": "Importing", - "noDescription": "This character card has no description.", + "noDescription": "This state package has no description yet.", "noIshiki": "No Ishiki provided", - "viewDetail": "View character card details", + "viewDetail": "View state package details", "memoryCount": "{count} memory items, optional {action}", "memoryCountExport": "export", "memoryCountImport": "import", @@ -2155,10 +2196,10 @@ "noIdentity": "No Identity provided", "noSkills": "No skills included", "cancel": "Cancel", - "readingCard": "Reading character card", - "dropOrClick": "Drop a character card or click to open", - "readFailed": "Character card read failed", - "importFailed": "Character card import failed" + "readingCard": "Reading Xiaoan state package", + "dropOrClick": "Drop a state package or click to open", + "readFailed": "State package read failed", + "importFailed": "State package import failed" } }, "onboarding": { @@ -2176,45 +2217,45 @@ "url": "Server URL", "key": "Access key", "connect": "Connect", - "connecting": "Connecting...", + "connecting": "Xiaoan is connecting…", "failed": "Failed to connect to LAN server" }, "name": { "title": "Your profile", - "subtitle": "Tell me what to call you, and name this assistant.", + "subtitle": "Tell Xiaoan what to call you. You can keep the default assistant name.", "userLabel": "What should I call you?", "placeholder": "Your name", - "agentLabel": "What should this Agent be called?", - "agentPlaceholder": "Minian", + "agentLabel": "Assistant display name", + "agentPlaceholder": "Xiaoan", "memoryTitle": "Memory system", "memoryHint": "When enabled, the system reviews chats after they end and turns them into memory. Because memory updates often, model cache hit rates may be lower. Turning memory off is usually cheaper.", "back": "Back", "next": "Next" }, "provider": { - "title": "Connect a model provider", - "subtitle": "Choose a provider and enter your API key to continue.", - "selectPlaceholder": "Choose provider", - "searchPlaceholder": "Search providers...", - "empty": "No providers found", + "title": "Connect Xiaoan", + "subtitle": "The built-in relay is selected by default. Paste your access key and Xiaoan will check available models.", + "selectPlaceholder": "Choose connection", + "searchPlaceholder": "Search connections…", + "empty": "No matching sources", "custom": "Custom...", "customName": "Name", "customNamePlaceholder": "e.g. my-provider", "customUrl": "API URL", "customUrlPlaceholder": "e.g. https://api.example.com/v1", - "keyLabel": "API Key", - "keyPlaceholder": "Paste your API Key", + "keyLabel": "Access key", + "keyPlaceholder": "Paste your access key", "test": "Test Connection", - "testing": "Connecting...", + "testing": "Xiaoan is checking the connection…", "testSuccess": "Connected", "testFailed": "Connection failed", "back": "Back", "next": "Next" }, "model": { - "title": "Choose your default model", - "subtitle": "This is the main model for everyday chat. Pick a capable model.", - "loading": "Fetching models...", + "title": "Xiaoan's default model", + "subtitle": "Xiaoan automatically uses the first available model. You can adjust it here.", + "loading": "Xiaoan is reading available models…", "empty": "No models found", "addedModels": "Added Models", "noAddedModels": "No models added yet", @@ -2236,9 +2277,9 @@ "cancelEdit": "Cancel", "saveEdit": "Save", "invalidNumber": "Enter a valid positive integer", - "utility": "Utility Model", + "utility": "Light task model", "utilityHint": "Summaries, fact extraction — pick a fast, cheap model", - "utilityLarge": "Utility Large", + "utilityLarge": "Deep task model", "utilityLargeHint": "Memory compilation, deep analysis — pick a capable model", "back": "Back", "next": "Next" @@ -2259,18 +2300,18 @@ "useDefault": "Use default", "back": "Back", "next": "Next", - "saving": "Saving..." + "saving": "Xiaoan is saving…" }, "tutorial": { "title": "You're all set", - "subtitle": "Start with a blank canvas: chat first, then add assistants, skills, and tools as you go.", + "subtitle": "Start with chat. Add Xiaoan modes, skills, and capabilities only when you need them.", "assistants": { - "title": "Assistants", - "desc": "Create assistants for different jobs. Switch models and personas without rewriting prompts every time." + "title": "Xiaoan Modes", + "desc": "Save research, writing, operations, or other work as modes. Switch model, voice, and workspace without rewriting instructions." }, "skills": { - "title": "Skills & MCP", - "desc": "Use Skills and MCP to connect files, commands, and external services—and reuse what works." + "title": "Capabilities & connections", + "desc": "Use skills and external connections to link files, commands, and services, then reuse what works." }, "workspace": { "title": "Local & workspace", @@ -2290,9 +2331,9 @@ "automationRuns": "Custom automations", "patrolRuns": "Patrols", "otherRuns": "Other background work", - "agentTabs": "Filter activity by Agent", - "previousAgent": "Show previous Agents", - "nextAgent": "Show more Agents", + "agentTabs": "Filter activity by mode", + "previousAgent": "Show previous modes", + "nextAgent": "Show more modes", "categoryTabs": "Filter by activity type", "emptyForCategory": "No activity in this category yet.", "patrolDone": "Patrol completed", @@ -2308,12 +2349,12 @@ "promote": "Keep as chat" }, "automation": { - "title": "Scheduled Tasks", - "empty": "No scheduled tasks yet.\nThe assistant will create them when needed.", - "emptyForAgent": "No scheduled tasks for this Agent yet.", - "agentTabs": "Filter automations by Agent", - "previousAgent": "Show previous Agents", - "nextAgent": "Show more Agents", + "title": "Scheduled runs", + "empty": "No scheduled runs yet.\nXiaoan will add them when needed.", + "emptyForAgent": "No scheduled runs for this assistant yet.", + "agentTabs": "Filter automations by mode", + "previousAgent": "Show previous modes", + "nextAgent": "Show more modes", "draftTitle": "Automation draft", "suggestionTitle": "Suggestion", "suggested": "Suggested", @@ -2329,12 +2370,12 @@ "newAutomation": "New automation", "newPrompt": "", "promptPlaceholder": "Write what you want {agent} to do", - "promptRequired": "Write what the Agent should do before enabling this automation.", + "promptRequired": "Write what Xiaoan should do before enabling this automation.", "createFailed": "Failed to create automation", - "agentRequired": "Choose an Agent first", + "agentRequired": "Choose a Xiaoan mode first", "field": { "label": "Name", - "agent": "Agent", + "agent": "Mode", "scheduleType": "Type", "schedule": "Time", "prompt": "Run prompt" @@ -2367,9 +2408,9 @@ } }, "executor": { - "agentSession": "Agent", + "agentSession": "Assistant run", "notify": "Notify", - "pluginAction": "Plugin action", + "pluginAction": "Capability action", "directAction": "Direct action" }, "edit": "Edit", @@ -2383,39 +2424,39 @@ }, "rightWorkspace": { "activity": { - "title": "Activity" + "title": "Background activity" }, "todo": { - "title": "Progress", + "title": "In progress", "waitForOutput": "Wait for output to finish" }, "brief": { - "title": "Brief", - "gaps": "Gaps", - "collected": "Collected", - "plan": "Plan", + "title": "Intent brief", + "gaps": "To clarify", + "collected": "Gathered", + "plan": "Steps", "nextStep": "Next step" }, "subagent": { - "title": "Sub-agents" + "title": "Helper clones" }, "workflow": { - "title": "Workflow", + "title": "Collaboration flow", "running": "Running {text}", - "agents": "{n} agents" + "agents": "{n} assistants" }, "session": { - "title": "This Conversation", - "cwd": "Workspace", - "model": "Model", - "files": "Files", - "authorizedFolders": "Authorized folders", - "addAuthorizedFolder": "Add authorized folder" + "title": "This chat", + "cwd": "Working folder", + "model": "Model in use", + "files": "Related files", + "authorizedFolders": "Allowed folders", + "addAuthorizedFolder": "Add allowed folder" }, "tabs": { "label": "Right workspace", - "sessionFiles": "Conversation Files", - "workspace": "Workspace" + "sessionFiles": "Chat files", + "workspace": "Folders" }, "sessionFiles": { "title": "Conversation Files", @@ -2428,11 +2469,11 @@ "copyPath": "Copy path", "copySelectedPaths": "Copy {n} paths", "sendToBridge": "Send to...", - "sendToBridgeLoading": "Loading Bridge conversations...", - "sendToBridgeEmpty": "No Bridge conversation available", - "sendToBridgeLoadFailed": "Failed to load Bridge conversations" + "sendToBridgeLoading": "Loading channel connection conversations...", + "sendToBridgeEmpty": "No channel connection conversation available", + "sendToBridgeLoadFailed": "Failed to load channel connection conversations" }, - "bridgeLoadFailed": "Failed to load Bridge conversations: {error}", + "bridgeLoadFailed": "Failed to load channel connection conversations: {error}", "sendSuccess": "Sent to {target}", "sendFailed": "Failed to send to {target}: {error}", "list": "Conversation file list", @@ -2449,27 +2490,27 @@ } }, "jian": { - "collapse": "Collapse Jian", - "expand": "Expand Jian" + "collapse": "Collapse notes", + "expand": "Expand notes" } }, "desk": { - "title": "Desk", + "title": "Workspace", "workspaceTitle": "Workspace", - "skills": "Skills", - "cwdSkills": "Project Skills", - "cwdSkillsEmpty": "No project skills in this folder", - "cwdSkillsHint": "Create a .agents/skills/ directory to get started", - "cwdSkillsDesc": "Skills follow workspace", - "cwdSkillsDrop": "Drop folder or .zip to install skills", - "deleteSkill": "Delete Skill", - "openInFinder": "Open Folder", - "jianLabel": "Jian", - "jianPlaceholder": "Write something to tell the assistant how to handle files in this folder...", - "execLogLabel": "Execution Log", + "skills": "Capabilities", + "cwdSkills": "Project skills", + "cwdSkillsEmpty": "No project skills in this folder yet", + "cwdSkillsHint": "Create a .agents/skills/ folder to add some", + "cwdSkillsDesc": "Capabilities travel with the workspace folder", + "cwdSkillsDrop": "Drop a folder or .zip to install skills", + "deleteSkill": "Delete skill", + "openInFinder": "Open in Finder", + "jianLabel": "Notes", + "jianPlaceholder": "A few lines on how files here should be handled…", + "execLogLabel": "Run log", "execLogDelete": "Delete this entry", - "emptyTitle": "Desk is empty", - "emptyHint": "The desk is the assistant's patrol workspace and the default directory for every chat.\nSet a folder in settings.", + "emptyTitle": "Workspace is still empty", + "emptyHint": "This folder is used for patrol and as the default chat directory.\nPick one in Settings.", "goToSettings": "Go to Settings", "fileLabel": "file", "openWithDefault": "Open with default app", @@ -2564,14 +2605,14 @@ "invalidJson": "Request body must be a JSON object", "sessionNotFound": "Session file not found", "stillStreaming": "{name} is still responding, please wait", - "agentNameEmpty": "Agent name cannot be empty", - "agentIdInvalid": "Agent ID contains invalid characters", - "agentAlreadyExists": "Agent \"{id}\" already exists", - "agentSwitching": "Agent switch in progress, please wait", - "agentNotFound": "Agent \"{id}\" not found or not initialized", + "agentNameEmpty": "Mode name cannot be empty", + "agentIdInvalid": "Mode ID contains invalid characters", + "agentAlreadyExists": "Mode \"{id}\" already exists", + "agentSwitching": "Mode switch in progress, please wait", + "agentNotFound": "Mode \"{id}\" not found or not initialized", "agentModelNotAvailable": "Model \"{model}\" configured for agent \"{id}\" is not available", "agentDeleteActive": "Cannot delete the currently active agent", - "agentNotExists": "Agent \"{id}\" does not exist", + "agentNotExists": "Mode \"{id}\" does not exist", "modelNoResponse": "Model returned no content. Please check API configuration and network connection.", "noActiveSession": "No active session", "compacting": "Compaction in progress", @@ -2608,7 +2649,7 @@ "fetchSource": "Source: {host}{path} ({format})\n\n", "fetchTimeout": "Fetch timeout ({sec}s): {url}", "fetchError": "Fetch error: {msg}", - "browserError": "Error: {msg}", + "browserError": "Browser failed: {msg}", "browserAlreadyRunning": "Browser is already running", "browserLaunched": "Browser launched", "browserNotRunning": "Browser is not running", @@ -2645,35 +2686,35 @@ "skillCannotAddSelfDir": "Cannot add Xiaoan's own skills directory", "skillInvalidName": "Invalid skill name", "skillExternalCannotDelete": "External skills cannot be deleted from here", - "skillNotExists": "Skill does not exist", + "skillNotExists": "Capability does not exist", "channelsDisabled": "Channel system is disabled by the user", - "channelReadNeedChannel": "Error: read requires channel parameter", + "channelReadNeedChannel": "Missing channel parameter for read", "channelNotExists": "Channel \"{channel}\" does not exist", "agentNotChannelMember": "You are not a member of channel \"{channel}\" and cannot send messages", "channelNoMessages": "(No messages in channel)", - "channelPostNeedParams": "Error: post requires channel and content parameters", + "channelPostNeedParams": "Missing channel or content parameter for post", "channelPosted": "Sent to #{channel}", - "channelCreateNeedParams": "Error: create requires name and members parameters", + "channelCreateNeedParams": "Missing name or members parameter for create", "channelCreated": "Channel #{name} ({id}) created, members: {members}", "channelCreateFailed": "Creation failed: {msg}", "channelNoJoined": "Not joined any channels yet", "unknownAction": "Unknown action: {action}", "cannotSelfDm": "Cannot send DM to yourself", "dmSent": "DM sent to {name}", - "agentNotFoundAvailable": "Agent \"{id}\" not found. Use the agent's id field value (not the display name). Available:\n{ids}", + "agentNotFoundAvailable": "Mode \"{id}\" not found. Use the mode id field value, not the display name. Available:\n{ids}", "noOtherAgents": "No other agents are currently available.", "cronNoJobs": "No scheduled tasks", - "cronAddNeedParams": "Error: add requires scheduleType, schedule, prompt parameters", - "cronEveryMustBeNumber": "Error: schedule for every type must be a positive integer (milliseconds)", + "cronAddNeedParams": "Missing scheduleType, schedule, or prompt for add", + "cronEveryMustBeNumber": "Every-type schedules must be positive integers in milliseconds", "cronCreated": "Scheduled task created: {label} ({id})", "cronConfirmed": "User confirmed, scheduled task created: {label} ({id})", "cronRejected": "User cancelled scheduled task \"{label}\"", "cronTimeout": "Scheduled task \"{label}\" confirmation timed out", "cronPendingConfirm": "Ready to create scheduled task: {label}, awaiting confirmation", - "cronRemoveNeedId": "Error: remove requires id parameter", + "cronRemoveNeedId": "Missing id parameter for remove", "cronJobNotFound": "Task {id} not found", "cronRemoved": "Task {id} removed", - "cronToggleNeedId": "Error: toggle requires id parameter", + "cronToggleNeedId": "Missing id parameter for toggle", "cronToggled": "Task {id} {state}", "cronEnabled": "enabled", "cronDisabled": "disabled", @@ -2719,20 +2760,20 @@ "subagentDispatched": "Sub-task dispatched (task-id: {taskId}). Results will be delivered when ready.", "subagentThreadQueued": "Sub-task dispatched (task-id: {taskId}). Instance {threadId} is busy; queued to run after the current task finishes.", "subagentThreadClosed": "Sub-agent instance closed: {threadId}", - "installSkillDisabled": "Skill installation is not enabled. Please enable \"Allow Agent to create/install skills\" in settings.", + "installSkillDisabled": "Capability installation is not enabled. Please enable \"Allow Xiaoan to create/install capabilities\" in settings.", "installSkillGithubDisabled": "GitHub skill fetching not enabled. Please enable \"Allow fetching skills from GitHub\" in settings.", "installSkillInvalidGithubUrl": "Invalid GitHub URL: {url}", "installSkillGithubApiFailed": "Cannot retrieve repo info (HTTP {status}). Please check the URL.", "installSkillGithubApiError": "GitHub API access failed: {msg}", "installSkillStarTooLow": "This repo only has {stars} stars (threshold {min}), installation rejected for safety.", "installSkillNoSkillMd": "SKILL.md not found in {owner}/{repo} (tried paths: {paths}).", - "installSkillSafetyFailed": "[Security] Skill safety review failed, installation blocked.\nReason: {reason}\n\nPlease inform the user about the safety review failure and the specific reason. Users can disable safety reviews in settings (not recommended).", + "installSkillSafetyFailed": "[Security] Capability safety review failed, installation blocked.\nReason: {reason}\n\nPlease inform the user about the safety review failure and the specific reason. Users can disable safety reviews in settings (not recommended).", "installSkillNameInvalid": "Invalid skill name{name}. Only letters, numbers, underscores, and hyphens are allowed.", - "installSkillSuccess": "Skill \"{name}\" installed successfully!\nSource: {source} (⭐ {stars} stars)\nReason: {reason}", + "installSkillSuccess": "Capability \"{name}\" installed successfully!\nSource: {source} (⭐ {stars} stars)\nReason: {reason}", "installSkillNeedInput": "Please provide github_url, local_path, fileId, or a source FileRef.", "installSkillNeedName": "skill_name is required when using skill_content mode.", - "installSkillSuccessLocal": "Skill \"{name}\" installed successfully!\nReason: {reason}", - "installSkillSizeLimit": "Skill content exceeds size limit ({size}KB > {max}KB)", + "installSkillSuccessLocal": "Capability \"{name}\" installed successfully!\nReason: {reason}", + "installSkillSizeLimit": "Capability content exceeds size limit ({size}KB > {max}KB)", "installSkillNoUtility": "Safety review unavailable: utility model not configured. Please configure it in settings.", "installSkillUtilityIncomplete": "Safety review unavailable: utility model config incomplete (missing API key or base URL). Please complete it in settings.", "installSkillSafetyPassed": "✓ Safety review passed", @@ -2760,12 +2801,12 @@ "noAvailableModel": "No available models. Please configure API key and models in settings first", "noActiveSessionPrompt": "No active session, please call createSession() first", "sessionNotInCache": "Session \"{path}\" not in cache", - "agentNotInitialized": "Agent \"{id}\" does not exist or is not initialized", + "agentNotInitialized": "Mode \"{id}\" does not exist or is not initialized", "resolveModelNoChatModel": "resolveModel: models.chat not specified, cannot select model", "resolveModelNotAvailable": "resolveModel: Model \"{id}\" not in available list", - "executeIsolatedNoModel": "executeIsolated: Agent \"{name}\" has no available model", - "bridgeAgentNoChatModel": "[bridge] Agent \"{name}\" has no models.chat configured", - "bridgeAgentModelNotAvailable": "[bridge] Model \"{model}\" configured for agent \"{name}\" is not in available list", + "executeIsolatedNoModel": "Mode \"{name}\" has no available model", + "bridgeAgentNoChatModel": "[connection] Mode \"{name}\" has no chat model configured", + "bridgeAgentModelNotAvailable": "[connection] Model \"{model}\" configured for mode \"{name}\" is not in available list", "noAgentsFound": "No agents found in agents/ directory, please create one first", "channelNotFoundById": "Channel \"{id}\" does not exist", "channelAlreadyExists": "Channel \"{id}\" already exists", @@ -2777,8 +2818,8 @@ "modelProviderIncomplete": "Provider \"{provider}\" for model \"{model}\" has incomplete config", "browserDesktopOnly": "Browser is only available in Electron desktop mode", "browserCmdTimeout": "Browser command timed out: {cmd}", - "agentExecNotInit": "Agent \"{id}\" does not exist or is not initialized", - "bridgeMgrNotStarted": "Bridge manager not started", + "agentExecNotInit": "Mode \"{id}\" does not exist or is not initialized", + "bridgeMgrNotStarted": "Channel connection manager is not started", "qqTokenFailed": "Failed to get access_token: {detail}", "missingApiProtocol": "Missing API protocol config", "missingApiKey": "Missing API Key", @@ -2823,8 +2864,8 @@ }, "dialog": { "selectFolder": "Select Working Folder", - "selectSkill": "Select Skill", - "selectPlugin": "Select Plugin", + "selectSkill": "Select capability", + "selectPlugin": "Select capability", "launchFailedTitle": "Xiaoan Launch Failed", "launchFailedBody": "Xiaoan v{version}\n\n{detail}\n\nDetailed log saved to:\n{logPath}\n\nPlease send this screenshot or log file to the developer.", "serverRestartFailed": "Xiaoan v{version}\n\nServer crashed and failed to restart.\n{error}\n\nPlease restart the application.", @@ -2852,7 +2893,7 @@ "tabPrompt": "System Prompt", "waitingConnection": "Waiting for connection...", "waitingServer": "Waiting for Server...", - "loading": "Loading...", + "loading": "Preparing Xiaoan...", "loadFailed": "Load failed", "refreshBtn": "Refresh", "clickRefresh": "Click Refresh above to load", @@ -2902,22 +2943,22 @@ "locale": "Language", "timezone": "Timezone", "timezoneDesc": "IANA timezone (e.g. Asia/Shanghai)", - "bridgeMediaPublicBaseUrl": "Bridge File Public URL", - "bridgeMediaPublicBaseUrlDesc": "Public HTTP/HTTPS base URL used by external platforms to fetch local staged files. Usually provided by a reverse proxy or tunnel. Leave empty to fall back to HANA_BRIDGE_PUBLIC_BASE_URL.", + "bridgeMediaPublicBaseUrl": "Channel File Public URL", + "bridgeMediaPublicBaseUrlDesc": "Public HTTP/HTTPS base URL used by external platforms to fetch local staged files. Usually provided by a reverse proxy or tunnel. Leave empty to fall back to the legacy public URL environment setting.", "computerUse": "Computer Use", - "computerUseDesc": "When enabled, new conversations expose the computer tool to the primary Agent. When disabled, new conversations cannot see it, and frozen old conversations are rejected if they call it.", + "computerUseDesc": "When enabled, new conversations expose the computer tool to the primary Xiaoan mode. When disabled, new conversations cannot see it, and frozen old conversations are rejected if they call it.", "thinkingBudget": "Thinking Depth", "memory": "Memory System", - "memoryDesc": "Enable/disable memory for current Agent", + "memoryDesc": "Enable/disable memory for the current mode", "memoryFacts": "Memory Facts", - "memoryFactsDesc": "Editable current-memory Facts for this Agent. Requires the Editable Memory experiment.", + "memoryFactsDesc": "Editable current-memory facts for this mode. Requires the Editable Memory experiment.", "memoryFactsDisabled": "Editable Memory experiment is disabled.", "experience": "Experience", - "experienceDesc": "Enable/disable the current Agent's experience library", - "agentName": "Agent Name", + "experienceDesc": "Enable/disable the current mode's experience library", + "agentName": "Mode Name", "userName": "User Name", "workingDir": "Working Directory", - "workingDirDesc": "Agent file system working directory", + "workingDirDesc": "Xiaoan file system working directory", "theme": "Theme", "chatModel": "Chat Model", "fileBackup": "File Backup", @@ -3001,7 +3042,7 @@ "cli": { "disconnected": "Disconnected", "wsError": "WebSocket error: {msg}", - "error": "Error: {msg}", + "error": "Could not complete the request: {msg}", "interrupted": "(interrupted)", "goodbye": "Goodbye", "helpTitle": "Command List", @@ -3036,9 +3077,9 @@ "sessionList": "Session List", "untitled": "(untitled)", "sessionUsage": "Usage: /session new | /session list", - "agentList": "Agent List", + "agentList": "Mode List", "agentSwitched": "Switched to {name}", - "currentAgent": "Current Agent:", + "currentAgent": "Current mode:", "jianTitle": "Notes", "jianEmpty": "No notes in this directory", "dirEmpty": "(empty)", @@ -3088,7 +3129,7 @@ "error.renderCrash": "Display error", "error.configParse": "Configuration file corrupted", "error.bridgeSendFailed": "Failed to send message to platform", - "error.skillSyncFailed": "Skill sync failed", + "error.skillSyncFailed": "Capability sync failed", "error.memoryCompileFailed": "Memory compilation failed", "error.dbError": "Database error", "error.serverAuthFailed": "Authentication failed", @@ -3134,8 +3175,8 @@ "hideTitle": "Hide \"{title}\"" }, "page": { - "notFound": "Plugin not found", - "loadFailed": "Plugin load failed", + "notFound": "Capability not found", + "loadFailed": "Capability failed to load", "retry": "Retry" }, "tab": { diff --git a/desktop/src/locales/ja.json b/desktop/src/locales/ja.json index 83143c8ed..502657f77 100644 --- a/desktop/src/locales/ja.json +++ b/desktop/src/locales/ja.json @@ -6,8 +6,8 @@ "defaultModelMissing": "先に設定でデフォルト画像モデルを設定してください。", "defaultModelInvalid": "デフォルト画像モデルが利用できません。設定で再設定してください。", "imageGenUnavailable": "画像生成ツールが無効です。先に設定で有効にしてください。", - "agentUnavailable": "メインエージェントが一時的に利用できないため、Agent 生成を開始できません。", - "disabled": "現在 Agent 生成は利用できません。" + "agentUnavailable": "メインアシスタントが一時的に利用できないため、カバーを生成できません。", + "disabled": "現在 Xiaoan でカバーを生成できません。" } }, "selection": { @@ -48,13 +48,13 @@ "createTitle": "新しいチャンネル", "createName": "チャンネル名", "createNamePlaceholder": "例: design-team", - "createMembers": "Agent メンバー", + "createMembers": "Xiaoan モードのメンバー", "createIntro": "説明", "createIntroPlaceholder": "このチャンネルの用途を書いてください...", "createCancel": "キャンセル", "createConfirm": "作成", "nameRequired": "チャンネル名を入力してください", - "minMembers": "Agent メンバーを2人以上選んでください", + "minMembers": "Xiaoan モードのメンバーを2人以上選んでください", "created": "チャンネル #{name} を作成しました", "createFailed": "作成に失敗しました", "alreadyExists": "チャンネルはすでに存在します", @@ -73,8 +73,8 @@ "removeMemberConfirm": "「{name}」をこのチャンネルから削除しますか?", "addMemberFailed": "メンバーの追加に失敗しました", "removeMemberFailed": "メンバーの削除に失敗しました", - "noAvailableMembers": "追加できる Agent はありません", - "agentActivity": "Agent の動き", + "noAvailableMembers": "追加できる Xiaoan モードはありません", + "agentActivity": "Xiaoan の動き", "agentSettings": "チャット設定", "toolPermission": "ツール権限", "agentIdle": "待機中", @@ -105,7 +105,7 @@ }, "skills": { "panel": { - "title": "Skills", + "title": "スキル", "allTab": "すべてのスキル", "agentTabs": "アシスタント別にスキルを表示", "previousAgent": "前のアシスタント", @@ -176,9 +176,9 @@ "unpinFailed": "ピン留め解除に失敗しました", "archive": "アーカイブ", "deletedAgent": { - "title": "Agent は削除済みです", - "description": "このチャットは読み取り専用の履歴として保持されます。続行すると、主 Agent で新しいセッションを作成し、古いコンテキストの圧縮を試みます。", - "continueButton": "主 Agent に移行", + "title": "Xiaoan モードは削除済みです", + "description": "このチャットは読み取り専用の履歴として保持されます。続行すると、既定の Xiaoan モードで新しいチャットを作成し、古いコンテキストの圧縮を試みます。", + "continueButton": "既定の Xiaoan モードに移行", "continuing": "移行し、セッション圧縮を試行中", "continueFailed": "移行に失敗しました", "continueCompactionFailed": "移行は完了しましたが、古いコンテキストの圧縮に失敗しました", @@ -389,7 +389,7 @@ "memoryOn": "記憶", "memoryOff": "このチャットでは記憶を使用しません", "memoryDisabled": "記憶はオフです", - "agentSelector": "エージェントを選択", + "agentSelector": "Xiaoan モードを選択", "placeholderTips": [ "/ を入力するとコマンドメニューが開きます", "@ でワークスペース内のファイルを参照できます", @@ -398,15 +398,15 @@ "ページ上のテキストを選択すると、フローティング入力欄が現れます", "アバター下のひし形ボタンで、この会話を記憶するか切り替えられます", "ワークスペースボタンでフォルダを切り替えたり追加したりできます", - "右側の「笺」パネルには、この会話のメモとコンテキストが保存されます", + "右側の「メモ」パネルには、この会話のメモとコンテキストが保存されます", "アバター下の小さな点をタップするとアシスタントを切り替えられます", "「操作前確認」モードは、アシスタントが行動する前に承認を求めます", "/rc を試してみて — スマホからパソコンを操作できます", "設定で「ビジョン補助」を有効にすると、テキストモデルも画像を理解できます", "新しい発見が欲しいですか?プラグインマーケットを覗いてみてください", "ファイルバックアップ機能で、安全面の不安を解消できます", - "仕事ではサブエージェントで Agent を連携、チャンネルは遊び用に", - "Agent ごとに専用の Skills を設定できます", + "仕事ではサブタスクで Xiaoan を連携し、チャンネルは軽い交流に", + "Xiaoan モードごとに専用スキルを設定できます", "晴れモードで日光浴を試してみて!", "ワークスペース内の Markdown ファイルは開いてそのまま編集できます", "アシスタントとしばらく過ごしたら、日記機能を試してみて!", @@ -855,9 +855,9 @@ "interface": "インターフェース", "general": "一般", "browser": "ブラウザ", - "agent": "エージェント", + "agent": "Xiaoan モード", "me": "自分", - "providers": "プロバイダー", + "providers": "モデル設定", "media": "メディア", "models": "モデル", "memory": "記憶", @@ -868,7 +868,7 @@ "sharing": "共有", "access": "アクセスとデバイス", "plugins": "プラグイン", - "about": "概要", + "about": "Xiaoan について", "security": "セキュリティ" }, "browser": { @@ -879,9 +879,9 @@ "clearCookiesHint": "内蔵ブラウザに保存された Cookies とサイトデータを消去します。", "clearCookiesButton": "消去", "clearCookiesSuccess": "Cookies を消去しました", - "agentTitle": "Agent の既定動作", + "agentTitle": "Xiaoan の既定動作", "agentOpenBehavior": "Web ページを開く時", - "agentOpenBehaviorHint": "Agent がブラウザを使う時に、現在のタブを使うか新しいタブを開くかを選びます。", + "agentOpenBehaviorHint": "Xiaoan がブラウザを使う時に、現在のタブを使うか新しいタブを開くかを選びます。", "agentOpenBehaviorCurrentTab": "現在のタブで操作", "agentOpenBehaviorNewTab": "新しいタブを開く" }, @@ -937,6 +937,10 @@ "appearance": { "title": "外観", "theme": "テーマ", + "xiaoanConsole": "小安 · 桜光", + "xiaoanConsoleMode": "ライト", + "xiaoanConsoleDark": "小安 · 聖夜", + "xiaoanConsoleDarkMode": "ダーク", "warmPaper": "温紙", "warmPaperMode": "ライト", "midnight": "蒼夜", @@ -1033,13 +1037,13 @@ "timezoneHint": "記憶のタイムスタンプや日記などに使用されます" }, "agent": { - "title": "エージェント", + "title": "Xiaoan モード", "changeAvatar": "変更", "removeAvatar": "アバターを削除", "avatarRemoved": "デフォルトのアバターに戻しました", "agentNameHint": "エージェントの呼び名", "identity": "アイデンティティ", - "identityHint": "このエージェントがどんな存在で、何が得意かを簡単に説明します。他のエージェントがこの情報を参照します", + "identityHint": "このモードがどんな作業に向いているかを短く書いてください。Xiaoan はモード間の協作でこれを参照します。", "ishiki": "意識", "ishikiHint": "アシスタントの性格、口調、振る舞いを定義します。「元」とは分離されており、互いに影響しません。", "publicIshiki": "パブリック意識", @@ -1050,8 +1054,8 @@ "modelUnavailableHint": "現在のモデルは利用可能リストにありません。別のモデルを選ぶか、プロバイダーページで設定を保存し直してモデルを取得してください。", "imageModel": "画像モデル", "imageModelHint": "画像生成で使用するモデル", - "addAgent": "新しいエージェント", - "createTitle": "新しいエージェント", + "addAgent": "新しいモード", + "createTitle": "新しい Xiaoan モード", "namePlaceholder": "名前をつけてください", "cancel": "キャンセル", "confirm": "作成", @@ -1061,19 +1065,19 @@ "createFailed": "作成に失敗しました", "switched": "{name} に切り替えました", "switchFailed": "切り替えに失敗しました", - "deleteBtn": "エージェントを削除", + "deleteBtn": "モードを削除", "deleteTitle1": "{name} を削除しますか?", "deleteDesc1": "すべての設定、記憶、会話が完全に削除されます。この操作は取り消せません。", "deleteNext": "続ける", "deleteTitle2": "確認のため「{name}」と入力してください", - "deletePlaceholder": "エージェント名を入力", + "deletePlaceholder": "モード名を入力", "deleteConfirm": "完全に削除", "deleteCancel": "キャンセル", "deleted": "{name} を削除しました", "deleteFailed": "削除に失敗しました", - "setPrimary": "主エージェントに設定", - "setPrimaryFailed": "主エージェントの設定に失敗しました", - "lastAgent": "エージェントは最低1つ必要です", + "setPrimary": "既定モードにする", + "setPrimaryFailed": "既定モードの設定に失敗しました", + "lastAgent": "Xiaoan モードは少なくとも 1 つ必要です", "yuan": "元", "yuanHint": "「元」はエージェントの潜在意識です。この土台からあなただけのパートナーを作り上げてください。", "tools": { @@ -1082,11 +1086,11 @@ "items": { "automation": { "label": "自動化", - "summary": "通知とプラグインのスケジュール動作を Agent が作成できるようにします" + "summary": "通知と拡張のスケジュール動作を Xiaoan が作成できるようにします" }, "beautify": { "label": "ページ美化", - "summary": "Agent にドキュメントカバーと美的な強化を生成させます" + "summary": "Xiaoan にドキュメントカバーと見た目の強化を生成させます" }, "browser": { "label": "ブラウザ", @@ -1106,11 +1110,11 @@ }, "update_settings": { "label": "設定を変更", - "summary": "Agent にアプリ設定の変更を許可" + "summary": "Xiaoan にアプリ設定の変更を許可" }, "workflow": { "label": "Workflow", - "summary": "Agent に複数 Agent のワークフロースクリプトをバックグラウンドタスクとして実行させる" + "summary": "Xiaoan に複数モードのワークフロースクリプトをバックグラウンドタスクとして実行させる" } } } @@ -1235,7 +1239,7 @@ "sandboxNetworkWin32Unsupported": "Windows のコマンドサンドボックスはファイル書き込みのみを隔離し、ネットワーク隔離には対応していません。コマンドは現在のユーザーのネットワーク権限で実行されます", "networkProxy": "アウトバウンドプロキシ", "networkProxyMode": "プロキシモード", - "networkProxyModeDesc": "アプリ内 API、OAuth、Bridge、Web リクエストはこのプロキシを使います", + "networkProxyModeDesc": "アプリ内 API、OAuth、チャンネル接続、Web リクエストはこのプロキシを使います", "networkProxySystem": "システム / 環境", "networkProxyManual": "手動プロキシ", "networkProxyDirect": "直接接続", @@ -1314,7 +1318,32 @@ "addCustom": "カスタムプロバイダーを追加", "noKey": "未設定", "remove": "削除", - "deleteConfirm": "プロバイダー {name} を削除しますか?関連するモデルも削除されます。" + "deleteConfirm": "プロバイダー {name} を削除しますか?関連するモデルも削除されます。", + "builtinRelay": { + "title": "Anxin Relay Hub", + "description": "通常利用では、モデルは内蔵リレー経由で接続されます。以下の手順でログインと設定を済ませると会話を始められます。", + "integrationNote": "ログイン、トークン作成、チャージは現在ブラウザーで行います。今後のバージョンでアプリ内アカウント連携を強化します。", + "guideTitle": "はじめる前に", + "stepLogin": "コンソールを開き、an520.xin アカウントに登録またはログイン", + "stepToken": "トークンページで API Key を作成してコピー", + "stepPaste": "下に Key を貼り付け、接続を確認", + "stepFetch": "「モデルを取得」をクリックして利用可能なモデル一覧を同期", + "stepRecharge": "残高が不足したらチャージします", + "consoleLabel": "アカウント", + "consoleHint": "初回は登録してください。ログイン済みならトークン作成へ進めます。", + "consoleAction": "コンソールを開く", + "tokenAction": "トークンを作成", + "rechargeLabel": "アカウント残高", + "rechargeHint": "リレーコンソールでチャージするとモデルサービスを継続できます。", + "rechargeAction": "チャージ", + "knownIssuesTitle": "既知の制限", + "issueBrowser": "ログイン、トークン、チャージにはまだシステムブラウザーが必要です。", + "issueManualKey": "API Key はリレーから手動でコピーして貼り付ける必要があり、自動取得や自動更新はできません。", + "issueNoBalance": "残高と利用量は表示されません。チャージ後の利用可否は手動で確認してください。", + "issueLegacyProviders": "以前設定した他のモデルソースが、エンジン側ではまだ読み込まれる場合があります(設定画面では非表示)。", + "issueChatPicker": "チャットと Xiaoan モードのモデル選択は、まだ完全にはリレーモデルに限定されていません。", + "issueIntegration": "Key 保存後にモデル取得や既定チャットモデル選択は自動実行されません。「モデルを取得」を手動で押してください。" + } }, "usage": { "title": "使用量", @@ -1356,7 +1385,7 @@ "automation": "Automation", "subagent": "Subagent", "compaction": "Compaction", - "plugin": "Plugin", + "plugin": "拡張", "utility": "Utility", "vision": "Vision", "unknown": "Unknown" @@ -1591,7 +1620,7 @@ "heartbeatUnit": "分", "heartbeatDesc": "アシスタントがワークスペースを自動チェックする間隔", "automationPermissionMode": "自動化の権限モード", - "automationPermissionModeDesc": "バックグラウンド自動化 Agent Run がツール呼び出しを自動審査するか、完全権限で実行するか、読み取り専用にするかを決めます。自動審査に通らない操作は拒否されます", + "automationPermissionModeDesc": "バックグラウンド自動化タスクがツール呼び出しを自動審査するか、完全権限で実行するか、読み取り専用にするかを決めます。自動審査に通らない操作は拒否されます", "homeFolder": "ワークスペース", "homeFolderDesc": "新しいチャット、巡回、cron タスクのデフォルト作業ディレクトリ", "homeFolderPlaceholder": "未設定(毎回手動で選択)", @@ -1610,10 +1639,10 @@ "skills": { "title": "スキル", "desc": "このエージェントのスキルを有効または無効にできます。有効にすると、会話の中で該当するスキルを自動的に認識して使用します。", - "manageTitle": "Installed Skills", - "agentConfigTitle": "Agent Skill Configuration", + "manageTitle": "インストール済みスキル", + "agentConfigTitle": "Xiaoan スキル設定", "configureFor": "Configuring:", - "userSkillsTitle": "Agent Skill Toggles", + "userSkillsTitle": "Xiaoan スキルトグル", "user": "ユーザースキル", "system": "システムスキル", "add": "スキルを追加", @@ -1640,7 +1669,7 @@ "compatAutoDiscovered": "自動検出", "compatNotInstalled": "未インストール", "learnCreate": "スキルを作成 / インストール", - "learnCreateDesc": "エージェントが新しいスキルを作成したり、GitHub、サーバーローカルパス、アップロード済みパッケージから共有スキルプールへインストールすることを許可します。デフォルトでは現在の Agent のみ有効になります", + "learnCreateDesc": "Xiaoan が新しいスキルを作成したり、GitHub、サーバーローカルパス、アップロード済みパッケージから共有スキルプールへインストールすることを許可します。デフォルトでは現在の Xiaoan モードのみ有効になります", "fetchRemote": "GitHub / ClawHub からスキルを取得", "fetchRemoteDesc": "有効にすると、エージェントは専門タスクの実行時にスキルを検索してインストールします。慎重にご使用ください", "safetyReview": "インストール前の安全審査", @@ -1674,8 +1703,8 @@ "statusDisabled": "無効", "fullAccessToggle": "フルアクセスプラグインを許可", "fullAccessDesc": "有効にすると、full-access を宣言したプラグインがシステムレベルの API(ルート、フック、Provider 等)を使用できます", - "devToolsToggle": "Agent のプラグイン開発ツールを許可", - "devToolsDesc": "有効にすると、Agent は開発用プラグインのインストール、再読み込み、有効化、無効化、削除を実行できます。通常インストールされたプラグインには影響しません。", + "devToolsToggle": "Xiaoan の拡張開発ツールを許可", + "devToolsDesc": "有効にすると、Xiaoan は開発用拡張のインストール、再読み込み、有効化、無効化、削除を実行できます。通常インストールされた拡張には影響しません。", "dropzone": "プラグインフォルダまたは .zip をここにドラッグしてインストール、またはクリックして選択", "statusRestricted": "フルアクセスが必要", "deleteConfirm": "プラグイン「{name}」を削除しますか?プラグインのデータは保持されます。", @@ -1778,7 +1807,7 @@ "stop": "Stop", "refresh": "Refresh Tools", "removeConfirm": "Remove this connector? Saved per-agent switches will remain but no longer take effect.", - "agentTitle": "Agent Configuration", + "agentTitle": "Xiaoan 設定", "agentConnectorDesc": "Enable for the selected agent, just like skills.", "saveError": "Save failed" }, @@ -1805,7 +1834,7 @@ "segmentLimitUnit": "文字" }, "bridge": { - "agent": "Agent", + "agent": "Xiaoan モード", "telegram": "Telegram", "telegramToken": "Bot Token", "telegramHint": "@BotFather から取得", @@ -1826,8 +1855,8 @@ "qqHint": "QQ ボットオープンプラットフォーム(q.qq.com/qqbot)から取得", "wechat": "WeChat", "wechatHint": "WeChat iLink プロトコルで個人アカウントに接続", - "wechatExclusive": "1 つの WeChat アカウントは 1 つの Agent にのみ紐付けできます。新しい Agent を紐付けると、以前のものは自動的に無効になります", - "wechatReplyWindowReminder": "WeChat の能動通知には、直近の受信メッセージまたは返信可能な時間枠が必要です。通知が届かない場合は、この Agent に WeChat メッセージを送って時間枠を有効にしてください。", + "wechatExclusive": "1 つの WeChat アカウントは 1 つの Xiaoan モードにのみ紐付けできます。新しいモードを紐付けると、以前のものは自動的に無効になります", + "wechatReplyWindowReminder": "WeChat の能動通知には、直近の受信メッセージまたは返信可能な時間枠が必要です。通知が届かない場合は、この Xiaoan モードに WeChat メッセージを送って時間枠を有効にしてください。", "wechatScan": "QR コードでログイン", "wechatRescan": "再ログイン", "wechatLoggedIn": "ログイン済み", @@ -1851,7 +1880,7 @@ "globalSettings": "グローバル設定", "agentSettings": "エージェント", "permissionMode": "権限モード", - "permissionModeDesc": "Bridge ボットがツール呼び出しを自動審査するか、完全権限で実行するか、読み取り専用にするかを決めます", + "permissionModeDesc": "チャンネルボットがツール呼び出しを自動審査するか、完全権限で実行するか、読み取り専用にするかを決めます", "permissionModeAuto": "自動審査", "permissionModeOperate": "完全権限", "permissionModeReadOnly": "読み取り専用モード", @@ -1903,9 +1932,9 @@ "onboarding": { "error": "操作に失敗しました。もう一度お試しください", "welcome": { - "title": "こんにちは、{name} です", - "subtitle": "あなた専用の AI アシスタントです。\n簡単なセットアップから始めましょう。", - "next": "はじめる" + "title": "Xiaoan へようこそ", + "subtitle": "デスクトップ AI アシスタントです。設定を完了するとすぐに会話を始められます。", + "next": "続ける" }, "remote": { "link": "既存のサーバーがありますか?LAN で接続", @@ -1916,11 +1945,11 @@ "failed": "LAN サーバーへの接続に失敗しました" }, "name": { - "title": "名前とメモリ", - "subtitle": "あなたをどう呼べばいいか、この Agent の名前も教えてください。", + "title": "あなたのプロフィール", + "subtitle": "Xiaoan があなたをどう呼べばよいか教えてください。既定のアシスタント名のままでも構いません。", "userLabel": "なんとお呼びすればいいですか?", "placeholder": "あなたの名前", - "agentLabel": "この Agent の名前は?", + "agentLabel": "アシスタント表示名", "agentPlaceholder": "Xiaoan", "memoryTitle": "メモリシステム", "memoryHint": "オンにすると、システムは会話終了時に内容を整理してメモリにします。メモリはよく更新されるため、モデルのキャッシュヒット率が下がることがあります。オフにすると通常はより安くなります。", @@ -1928,11 +1957,11 @@ "next": "次へ" }, "provider": { - "title": "AI プロバイダーを選択", - "subtitle": "動作するには AI モデルプロバイダーが必要です。\nプロバイダーを選んで API Key を入力してください。", - "selectPlaceholder": "プロバイダーを選択", - "searchPlaceholder": "プロバイダーを検索...", - "empty": "プロバイダーが見つかりません", + "title": "Xiaoan を接続", + "subtitle": "既定では内蔵リレーを使います。アクセスキーを貼り付けると、Xiaoan が利用可能なモデルを確認します。", + "selectPlaceholder": "接続方法を選択", + "searchPlaceholder": "接続方法を検索…", + "empty": "一致する接続がありません", "custom": "カスタム...", "customName": "名前", "customNamePlaceholder": "例: my-provider", @@ -2016,10 +2045,16 @@ "desc": "任意のフォルダに jian.md を置いて指示を書いてください。\n巡回時に見つけて自動的に実行します。" }, "agents": { - "title": "複数 Agent の協作", - "desc": "異なる Agent がそれぞれのタスクを担当し、作業を引き継げます。\n複雑な作業を分解するのに向いています。" + "title": "Xiaoan モード", + "desc": "調査、執筆、運用などの作業をモードとして保存し、必要なときに切り替えます。" + }, + "finish": "Xiaoan を開く", + "subtitle": "まずは会話から始めましょう。必要になったら Xiaoan モード、スキル、ツールを追加できます。", + "assistants": { + "title": "Xiaoan モード", + "desc": "調査、執筆、運用などの作業をモードとして保存できます。モデル、話し方、ワークスペースを毎回説明し直さずに切り替えられます。" }, - "finish": "さあ、始めましょう" + "finishing": "開いています…" } }, "activity": { @@ -2032,9 +2067,9 @@ "automationRuns": "カスタム自動化", "patrolRuns": "巡回", "otherRuns": "その他のバックグラウンド作業", - "agentTabs": "Agent で活動を絞り込み", - "previousAgent": "前の Agent を表示", - "nextAgent": "さらに Agent を表示", + "agentTabs": "Xiaoan モードで活動を絞り込み", + "previousAgent": "前の Xiaoan モードを表示", + "nextAgent": "さらに Xiaoan モードを表示", "categoryTabs": "活動タイプで絞り込み", "emptyForCategory": "この分類にはまだ活動がありません。", "patrolDone": "巡回完了", @@ -2052,10 +2087,10 @@ "automation": { "title": "スケジュールタスク", "empty": "スケジュールタスクはまだありません。\n必要に応じてアシスタントが作成します。", - "emptyForAgent": "この Agent にはスケジュールタスクがまだありません。", - "agentTabs": "Agent で自動化を絞り込み", - "previousAgent": "前の Agent を表示", - "nextAgent": "さらに Agent を表示", + "emptyForAgent": "この Xiaoan モードにはスケジュールタスクがまだありません。", + "agentTabs": "Xiaoan モードで自動化を絞り込み", + "previousAgent": "前の Xiaoan モードを表示", + "nextAgent": "さらに Xiaoan モードを表示", "draftTitle": "自動化ドラフト", "suggestionTitle": "提案", "suggested": "提案", @@ -2071,12 +2106,12 @@ "newAutomation": "新しい自動化", "newPrompt": "", "promptPlaceholder": "{agent} にしてほしいことを書いてください", - "promptRequired": "この自動化を有効にする前に、Agent にしてほしいことを書いてください。", + "promptRequired": "この自動化を有効にする前に、Xiaoan にしてほしいことを書いてください。", "createFailed": "自動化の作成に失敗しました", - "agentRequired": "先に Agent を選択してください", + "agentRequired": "先に Xiaoan モードを選択してください", "field": { "label": "名前", - "agent": "Agent", + "agent": "Xiaoan モード", "scheduleType": "種類", "schedule": "時間", "prompt": "実行内容" @@ -2163,11 +2198,11 @@ "copyPath": "パスをコピー", "copySelectedPaths": "{n} 件のパスをコピー", "sendToBridge": "送信先...", - "sendToBridgeLoading": "Bridge の会話を読み込み中...", - "sendToBridgeEmpty": "送信できる Bridge 会話がありません", - "sendToBridgeLoadFailed": "Bridge 会話の読み込みに失敗しました" + "sendToBridgeLoading": "チャンネル会話を読み込み中...", + "sendToBridgeEmpty": "送信できるチャンネル会話がありません", + "sendToBridgeLoadFailed": "チャンネル会話の読み込みに失敗しました" }, - "bridgeLoadFailed": "Bridge 会話の読み込みに失敗しました:{error}", + "bridgeLoadFailed": "チャンネル会話の読み込みに失敗しました:{error}", "sendSuccess": "{target} に送信しました", "sendFailed": "{target} への送信に失敗しました:{error}", "list": "会話ファイル一覧", @@ -2454,7 +2489,7 @@ "subagentDispatched": "サブタスクを送信しました (task-id: {taskId})。完了後、結果は自動的に届きます。", "subagentThreadQueued": "サブタスクを送信しました (task-id: {taskId})。インスタンス {threadId} は処理中のため、キューに入れました。現在のタスク完了後に実行されます。", "subagentThreadClosed": "サブエージェントインスタンスを閉じました: {threadId}", - "installSkillDisabled": "スキルのインストール機能が有効になっていません。設定で「Agent のスキル作成/インストールを許可」を有効にしてください。", + "installSkillDisabled": "スキルのインストール機能が有効になっていません。設定で「Xiaoan のスキル作成/インストールを許可」を有効にしてください。", "installSkillGithubDisabled": "GitHub スキル取得が有効になっていません。設定で「GitHub からのスキル取得を許可」を有効にしてください。", "installSkillInvalidGithubUrl": "無効な GitHub URL:{url}", "installSkillGithubApiFailed": "リポジトリ情報を取得できません(HTTP {status})。URL を確認してください。", @@ -2656,9 +2691,9 @@ "sessionList": "セッション一覧", "untitled": "(無題)", "sessionUsage": "使い方: /session new | /session list", - "agentList": "Agent 一覧", + "agentList": "Xiaoan モード一覧", "agentSwitched": "{name} に切り替えました", - "currentAgent": "現在の Agent:", + "currentAgent": "現在の Xiaoan モード:", "jianTitle": "付箋", "jianEmpty": "このディレクトリに付箋はありません", "dirEmpty": "(空)", @@ -2708,7 +2743,7 @@ "error.renderCrash": "Display error", "error.configParse": "Configuration file corrupted", "error.bridgeSendFailed": "Failed to send message to platform", - "error.skillSyncFailed": "Skill sync failed", + "error.skillSyncFailed": "スキル同期に失敗しました", "error.memoryCompileFailed": "Memory compilation failed", "error.dbError": "Database error", "error.serverAuthFailed": "Authentication failed", diff --git a/desktop/src/locales/ko.json b/desktop/src/locales/ko.json index dd23ac0d7..7ae69e6b6 100644 --- a/desktop/src/locales/ko.json +++ b/desktop/src/locales/ko.json @@ -6,8 +6,8 @@ "defaultModelMissing": "먼저 설정에서 기본 이미지 모델을 구성하세요.", "defaultModelInvalid": "기본 이미지 모델을 사용할 수 없습니다. 설정에서 다시 구성하세요.", "imageGenUnavailable": "이미지 생성 도구가 비활성화되어 있습니다. 먼저 설정에서 활성화하세요.", - "agentUnavailable": "메인 에이전트를 일시적으로 사용할 수 없어 Agent 생성을 시작할 수 없습니다.", - "disabled": "현재 Agent 생성을 사용할 수 없습니다." + "agentUnavailable": "기본 어시스턴트를 일시적으로 사용할 수 없어 커버를 생성할 수 없습니다.", + "disabled": "현재 Xiaoan으로 커버를 생성할 수 없습니다." } }, "selection": { @@ -48,13 +48,13 @@ "createTitle": "새 채널", "createName": "채널 이름", "createNamePlaceholder": "예: design-team", - "createMembers": "Agent 멤버", + "createMembers": "Xiaoan 모드 멤버", "createIntro": "설명", "createIntroPlaceholder": "이 채널의 용도를 설명해 주세요...", "createCancel": "취소", "createConfirm": "만들기", "nameRequired": "채널 이름을 입력해 주세요", - "minMembers": "Agent 멤버를 최소 2명 이상 선택해 주세요", + "minMembers": "Xiaoan 모드 멤버를 최소 2명 이상 선택해 주세요", "created": "채널 #{name} 생성 완료", "createFailed": "생성 실패", "alreadyExists": "이미 존재하는 채널이에요", @@ -73,8 +73,8 @@ "removeMemberConfirm": "\"{name}\"을(를) 이 채널에서 제거할까요?", "addMemberFailed": "멤버 추가 실패", "removeMemberFailed": "멤버 제거 실패", - "noAvailableMembers": "추가할 수 있는 Agent가 없어요", - "agentActivity": "Agent 상태", + "noAvailableMembers": "추가할 수 있는 Xiaoan 모드가 없어요", + "agentActivity": "Xiaoan 활동", "agentSettings": "채팅 설정", "toolPermission": "도구 권한", "agentIdle": "대기 중", @@ -105,7 +105,7 @@ }, "skills": { "panel": { - "title": "Skills", + "title": "스킬", "allTab": "모든 스킬", "agentTabs": "어시스턴트별 스킬 보기", "previousAgent": "이전 어시스턴트", @@ -176,9 +176,9 @@ "unpinFailed": "세션 고정 해제 실패", "archive": "보관", "deletedAgent": { - "title": "Agent가 삭제됨", - "description": "이 대화는 읽기 전용 기록으로 유지됩니다. 계속하면 기본 Agent에서 새 세션을 만들고 이전 컨텍스트 압축을 시도합니다.", - "continueButton": "기본 Agent로 이전", + "title": "Xiaoan 모드가 삭제됨", + "description": "이 대화는 읽기 전용 기록으로 유지됩니다. 계속하면 기본 Xiaoan 모드에서 새 대화를 만들고 이전 컨텍스트 압축을 시도합니다.", + "continueButton": "기본 Xiaoan 모드로 이전", "continuing": "이전하고 세션 압축을 시도하는 중", "continueFailed": "이전 실패", "continueCompactionFailed": "이전은 완료되었지만 이전 컨텍스트 압축에 실패했습니다", @@ -389,7 +389,7 @@ "memoryOn": "기억", "memoryOff": "이 대화에서는 기억을 사용하지 않아요", "memoryDisabled": "기억이 꺼져 있어요", - "agentSelector": "에이전트 선택", + "agentSelector": "Xiaoan 모드 선택", "placeholderTips": [ "/ 를 입력하면 명령 메뉴가 열려요", "@ 로 워크스페이스 안의 파일을 참조할 수 있어요", @@ -398,15 +398,15 @@ "페이지에서 텍스트를 선택하면 임시 입력창이 떠요", "아바타 아래 마름모 버튼으로 이번 대화의 기억을 켜고 끌 수 있어요", "워크스페이스 버튼으로 폴더를 바꾸거나 추가할 수 있어요", - "오른쪽 「笺」 패널에는 이번 대화의 메모와 컨텍스트가 있어요", + "오른쪽 「메모」 패널에는 이번 대화의 메모와 컨텍스트가 있어요", "아바타 아래 작은 원을 탭하면 도우미를 바꿀 수 있어요", "「작업 전 확인」 모드는 도우미가 행동하기 전에 동의를 구해요", "/rc 를 입력해서 휴대폰으로 컴퓨터를 조작해 보세요", "설정에서 「시각 보조」를 켜면 텍스트 모델도 이미지를 이해해요", "새로운 걸 발견하고 싶다면 플러그인 마켓을 둘러보세요", "파일 백업 기능으로 안전에 대한 걱정을 덜 수 있어요", - "업무에서는 서브에이전트로 Agent 협업을, 채널은 즐거움을 위해", - "Agent마다 독립된 Skills 를 설정할 수 있어요", + "업무에서는 하위 작업으로 Xiaoan을 협업시키고, 채널은 가볍게 사용하세요", + "각 Xiaoan 모드마다 독립된 스킬을 설정할 수 있어요", "맑음 모드에서 일광욕을 즐겨 보세요!", "워크스페이스의 Markdown 파일은 열면 바로 편집할 수 있어요", "도우미와 한참 지내봤다면, 일기 기능을 써 보세요!", @@ -855,9 +855,9 @@ "interface": "인터페이스", "general": "일반", "browser": "브라우저", - "agent": "에이전트", + "agent": "Xiaoan 모드", "me": "나", - "providers": "제공자", + "providers": "모델 설정", "media": "미디어", "models": "모델", "memory": "기억", @@ -868,7 +868,7 @@ "sharing": "공유", "access": "접근 및 기기", "plugins": "플러그인", - "about": "소개", + "about": "Xiaoan 소개", "security": "보안" }, "browser": { @@ -879,9 +879,9 @@ "clearCookiesHint": "내장 브라우저에 저장된 Cookies와 사이트 데이터를 지웁니다.", "clearCookiesButton": "지우기", "clearCookiesSuccess": "Cookies를 지웠어요", - "agentTitle": "Agent 기본 동작", + "agentTitle": "Xiaoan 기본 동작", "agentOpenBehavior": "웹페이지를 열 때", - "agentOpenBehaviorHint": "Agent가 브라우저를 사용할 때 현재 탭을 쓸지 새 탭을 열지 선택합니다.", + "agentOpenBehaviorHint": "Xiaoan이 브라우저를 사용할 때 현재 탭을 쓸지 새 탭을 열지 선택합니다.", "agentOpenBehaviorCurrentTab": "현재 탭에서 작업", "agentOpenBehaviorNewTab": "새 탭 열기" }, @@ -937,6 +937,10 @@ "appearance": { "title": "외관", "theme": "테마", + "xiaoanConsole": "샤오안 · 벚꽃 빛", + "xiaoanConsoleMode": "라이트", + "xiaoanConsoleDark": "샤오안 · 성야", + "xiaoanConsoleDarkMode": "다크", "warmPaper": "따뜻한 종이", "warmPaperMode": "라이트", "midnight": "아오이 밤", @@ -1033,13 +1037,13 @@ "timezoneHint": "기억 타임스탬프, 일기 항목 등에 사용돼요" }, "agent": { - "title": "에이전트", + "title": "Xiaoan 모드", "changeAvatar": "변경", "removeAvatar": "아바타 제거", "avatarRemoved": "기본 아바타로 복원됨", "agentNameHint": "불리는 이름", "identity": "정체성", - "identityHint": "이 에이전트가 누구이고 무엇을 잘하는지 짧게 설명해 주세요. 다른 에이전트가 이를 참고해요", + "identityHint": "이 모드가 어떤 일에 적합한지 짧게 적어 주세요. Xiaoan은 모드 간 협업 때 이 내용을 참고합니다.", "ishiki": "의식", "ishikiHint": "어시스턴트의 성격, 어조, 행동을 정의해요. 「元」과는 분리되어 있으며 서로 영향을 주지 않아요.", "publicIshiki": "공개 의식", @@ -1050,8 +1054,8 @@ "modelUnavailableHint": "현재 모델이 사용 가능한 목록에 없어요. 다른 모델을 선택하거나 Provider 페이지에서 설정을 다시 저장하고 모델을 가져오세요.", "imageModel": "이미지 모델", "imageModelHint": "이미지 생성에 사용하는 모델", - "addAgent": "새 에이전트", - "createTitle": "새 에이전트", + "addAgent": "새 모드", + "createTitle": "새 Xiaoan 모드", "namePlaceholder": "이름을 지어주세요", "cancel": "취소", "confirm": "만들기", @@ -1061,19 +1065,19 @@ "createFailed": "생성 실패", "switched": "{name}(으)로 전환됨", "switchFailed": "전환 실패", - "deleteBtn": "에이전트 삭제", + "deleteBtn": "모드 삭제", "deleteTitle1": "{name}을(를) 삭제할까요?", "deleteDesc1": "모든 설정, 기억, 대화가 영구적으로 삭제돼요. 이 작업은 되돌릴 수 없어요.", "deleteNext": "계속", "deleteTitle2": "확인하려면 \"{name}\"을(를) 입력해 주세요", - "deletePlaceholder": "에이전트 이름 입력", + "deletePlaceholder": "모드 이름 입력", "deleteConfirm": "영구 삭제", "deleteCancel": "취소", "deleted": "{name} 삭제됨", "deleteFailed": "삭제 실패", - "setPrimary": "주 에이전트로 설정", - "setPrimaryFailed": "주 에이전트 설정 실패", - "lastAgent": "에이전트는 최소 한 명 유지해야 해요", + "setPrimary": "기본 모드로 설정", + "setPrimaryFailed": "기본 모드 설정 실패", + "lastAgent": "Xiaoan 모드는 최소 하나 유지해야 해요", "yuan": "元", "yuanHint": "「元」은 에이전트의 잠재의식이에요. 이 기반 위에 당신만의 동반자를 만들어 보세요.", "tools": { @@ -1082,11 +1086,11 @@ "items": { "automation": { "label": "자동화", - "summary": "Agent가 알림과 플러그인 예약 동작을 만들 수 있게 합니다" + "summary": "Xiaoan이 알림과 확장 예약 동작을 만들 수 있게 합니다" }, "beautify": { "label": "페이지 미화", - "summary": "Agent가 문서 커버와 미적 보강을 생성할 수 있게 합니다" + "summary": "Xiaoan이 문서 커버와 시각 보강을 생성할 수 있게 합니다" }, "browser": { "label": "브라우저", @@ -1106,11 +1110,11 @@ }, "update_settings": { "label": "설정 변경", - "summary": "Agent가 앱 설정을 수정할 수 있도록 허용" + "summary": "Xiaoan이 앱 설정을 수정할 수 있도록 허용" }, "workflow": { "label": "Workflow", - "summary": "Agent가 다중 에이전트 워크플로 스크립트를 백그라운드 작업으로 실행하도록 허용" + "summary": "Xiaoan이 여러 모드 워크플로 스크립트를 백그라운드 작업으로 실행하도록 허용" } } } @@ -1149,7 +1153,7 @@ "lanToggle": "LAN 기기 접근 허용", "lanHint": "같은 LAN의 휴대폰과 컴퓨터가 이 Xiaoan Server에 접근할 수 있어요", "port": "고정 포트", - "portHint": "북마크와 QR 코드에 사용합니다. 변경 후 Minian을 다시 시작해야 적용돼요", + "portHint": "북마크와 QR 코드에 사용합니다. 변경 후 Xiaoan을 다시 시작해야 적용돼요", "mobileUrl": "모바일 URL", "mobileUrlLanHint": "휴대폰 브라우저에서 이 주소를 여세요", "mobileUrlLocalHint": "현재는 이 컴퓨터에서만 접속할 수 있어요", @@ -1162,7 +1166,7 @@ "noLanAddresses": "LAN 주소를 찾지 못했어요", "qrCode": "QR 코드", "qrCodeHint": "스캔해서 PWA를 엽니다", - "restartRequired": "포트 변경을 저장했습니다. Minian을 다시 시작하면 적용됩니다", + "restartRequired": "포트 변경을 저장했습니다. Xiaoan을 다시 시작하면 적용됩니다", "saveNetwork": "접근 설정 저장", "mobileKeys": "모바일 키", "generateMobileKey": "모바일 접근 키 생성", @@ -1187,15 +1191,15 @@ "remoteServerKeyHint": "그 서버 설정 페이지에서 생성한 컴퓨터 접근 키를 입력하세요", "remoteServerConnected": "LAN 서버 연결됨", "remoteServerFailed": "연결 실패", - "remoteConnection": "원격 Minian에 연결됨", - "remoteConnectionDesc": "접근 및 기기의 로컬 계정, 비밀번호, LAN 스위치, 접근 키는 로컬 Minian에만 속합니다.", + "remoteConnection": "원격 Xiaoan에 연결됨", + "remoteConnectionDesc": "접근 및 기기의 로컬 계정, 비밀번호, LAN 스위치, 접근 키는 로컬 Xiaoan에만 속합니다.", "remoteConnectionName": "연결 이름", "remoteConnectionKind": "연결 유형", "remoteConnectionUrl": "서버 URL", "remoteConnectionUnknown": "알 수 없음", - "remoteLocalOnlyNote": "이 컴퓨터의 LAN 접근, 기기 키, 로컬 계정을 관리하려면 먼저 로컬 Minian으로 돌아가세요.", - "returnToLocal": "로컬 Minian으로 돌아가기", - "returnedToLocal": "로컬 Minian으로 돌아왔습니다", + "remoteLocalOnlyNote": "이 컴퓨터의 LAN 접근, 기기 키, 로컬 계정을 관리하려면 먼저 로컬 Xiaoan으로 돌아가세요.", + "returnToLocal": "로컬 Xiaoan으로 돌아가기", + "returnedToLocal": "로컬 Xiaoan으로 돌아왔습니다", "localConnectionUnavailable": "로컬 Xiaoan 연결이 아직 준비되지 않았습니다", "pairedDevices": "페어링된 기기", "noDevices": "페어링된 기기 없음", @@ -1235,7 +1239,7 @@ "sandboxNetworkWin32Unsupported": "Windows 명령 샌드박스는 파일 쓰기만 격리하며 네트워크 격리는 지원하지 않아요. 명령은 현재 사용자 네트워크 권한으로 실행돼요", "networkProxy": "아웃바운드 프록시", "networkProxyMode": "프록시 모드", - "networkProxyModeDesc": "앱 내 API, OAuth, Bridge, 웹 요청이 이 아웃바운드 프록시를 사용해요", + "networkProxyModeDesc": "앱 내 API, OAuth, 채널 연결, 웹 요청이 이 아웃바운드 프록시를 사용해요", "networkProxySystem": "시스템 / 환경", "networkProxyManual": "수동 프록시", "networkProxyDirect": "직접 연결", @@ -1310,11 +1314,36 @@ "setupHint": "API Key를 입력해서 이 Provider를 활성화하세요", "configInvalid": "이 Provider 설정이 손상되어 임시로 건너뛰었어요. 설정을 다시 저장하거나 삭제 후 다시 만들어 주세요.", "configIncomplete": "이 Provider 설정이 완전하지 않아요. API Key, Base URL을 채우고 모델을 추가하거나 가져오세요.", - "selectHint": "Provider를 선택하면 상세 정보를 볼 수 있어요", + "selectHint": "모델 소스를 선택하면 상세 정보를 볼 수 있어요", "addCustom": "커스텀 Provider 추가", "noKey": "미설정", "remove": "제거", - "deleteConfirm": "Provider {name}을(를) 삭제할까요? 연결된 모델도 함께 제거돼요." + "deleteConfirm": "Provider {name}을(를) 삭제할까요? 연결된 모델도 함께 제거돼요.", + "builtinRelay": { + "title": "Anxin Relay Hub", + "description": "일상 사용에서는 모델이 내장 릴레이를 통해 연결됩니다. 아래 단계대로 로그인하고 설정하면 바로 대화를 시작할 수 있어요.", + "integrationNote": "로그인, 토큰 생성, 충전은 현재 브라우저에서 진행됩니다. 이후 버전에서 앱 안 계정 통합을 강화할 예정입니다.", + "guideTitle": "시작 전", + "stepLogin": "콘솔을 열고 an520.xin 계정에 가입하거나 로그인", + "stepToken": "토큰 페이지에서 API Key를 만들고 복사", + "stepPaste": "아래에 Key를 붙여넣고 연결을 확인", + "stepFetch": "모델 가져오기를 눌러 사용 가능한 모델 목록 동기화", + "stepRecharge": "잔액이 부족하면 충전으로 보충", + "consoleLabel": "계정 로그인", + "consoleHint": "처음이라면 먼저 가입하세요. 이미 로그인했다면 토큰 생성으로 바로 가면 됩니다.", + "consoleAction": "콘솔 열기", + "tokenAction": "토큰 만들기", + "rechargeLabel": "계정 잔액", + "rechargeHint": "릴레이 콘솔에서 충전하면 모델 서비스를 계속 사용할 수 있어요.", + "rechargeAction": "충전", + "knownIssuesTitle": "알려진 제한 사항", + "issueBrowser": "로그인, 토큰, 충전은 아직 시스템 브라우저가 필요합니다.", + "issueManualKey": "API Key는 릴레이에서 직접 복사해 붙여넣어야 하며 자동으로 가져오거나 갱신할 수 없습니다.", + "issueNoBalance": "잔액과 사용량은 표시되지 않습니다. 충전 후 사용 가능 여부는 직접 확인해야 합니다.", + "issueLegacyProviders": "예전에 설정한 다른 모델 소스가 엔진에는 여전히 로드될 수 있습니다(설정 화면에서는 숨김).", + "issueChatPicker": "채팅과 Xiaoan 모드의 모델 선택기가 아직 릴레이 모델로 완전히 제한되지는 않습니다.", + "issueIntegration": "Key 저장 후 모델을 자동으로 가져오거나 기본 대화 모델을 추천하지 않습니다. 모델 가져오기를 직접 눌러 주세요." + } }, "usage": { "title": "사용량", @@ -1356,7 +1385,7 @@ "automation": "Automation", "subagent": "Subagent", "compaction": "Compaction", - "plugin": "Plugin", + "plugin": "확장", "utility": "Utility", "vision": "Vision", "unknown": "Unknown" @@ -1591,7 +1620,7 @@ "heartbeatUnit": "분", "heartbeatDesc": "어시스턴트가 작업 공간을 자동으로 확인하는 주기", "automationPermissionMode": "자동화 권한 모드", - "automationPermissionModeDesc": "백그라운드 자동화 Agent Run이 도구 호출을 자동 검토할지, 전체 권한으로 실행할지, 읽기 전용으로 둘지 정합니다. 자동 검토를 통과하지 못한 작업은 거부됩니다", + "automationPermissionModeDesc": "백그라운드 자동화 작업이 도구 호출을 자동 검토할지, 전체 권한으로 실행할지, 읽기 전용으로 둘지 정합니다. 자동 검토를 통과하지 못한 작업은 거부됩니다", "homeFolder": "작업 공간", "homeFolderDesc": "새 대화, 순찰, 예약 작업의 기본 작업 디렉토리", "homeFolderPlaceholder": "미설정 (매번 수동 선택)", @@ -1610,10 +1639,10 @@ "skills": { "title": "스킬", "desc": "이 에이전트의 스킬을 켜거나 끌 수 있어요. 활성화하면 에이전트가 대화에서 자동으로 해당 스킬을 인식하고 사용해요.", - "manageTitle": "Installed Skills", - "agentConfigTitle": "Agent Skill Configuration", + "manageTitle": "설치된 스킬", + "agentConfigTitle": "Xiaoan 스킬 설정", "configureFor": "Configuring:", - "userSkillsTitle": "Agent Skill Toggles", + "userSkillsTitle": "Xiaoan 스킬 토글", "user": "사용자 스킬", "system": "시스템 스킬", "add": "스킬 추가", @@ -1640,7 +1669,7 @@ "compatAutoDiscovered": "자동 발견됨", "compatNotInstalled": "미설치", "learnCreate": "스킬 생성 / 설치", - "learnCreateDesc": "에이전트가 새 스킬을 작성하거나 GitHub, 서버 로컬 경로, 업로드된 패키지에서 공유 스킬 풀로 설치하는 것을 허용해요. 기본적으로 현재 Agent에만 활성화돼요", + "learnCreateDesc": "Xiaoan이 새 스킬을 작성하거나 GitHub, 서버 로컬 경로, 업로드된 패키지에서 공유 스킬 풀로 설치하는 것을 허용해요. 기본적으로 현재 Xiaoan 모드에만 활성화돼요", "fetchRemote": "GitHub / ClawHub에서 스킬 가져오기", "fetchRemoteDesc": "활성화하면 에이전트가 전문 작업 수행 시 적합한 스킬을 검색하고 설치해요. 주의해서 사용하세요", "safetyReview": "설치 전 안전 검토", @@ -1674,8 +1703,8 @@ "statusDisabled": "비활성화", "fullAccessToggle": "전체 권한 플러그인 허용", "fullAccessDesc": "활성화하면 full-access를 선언한 플러그인이 시스템 수준 API(라우트, 훅, Provider 등)를 사용할 수 있어요", - "devToolsToggle": "Agent 플러그인 개발 도구 허용", - "devToolsDesc": "활성화하면 Agent가 개발 플러그인을 설치, 다시 로드, 활성화, 비활성화, 제거할 수 있어요. 일반 설치 플러그인에는 영향을 주지 않아요.", + "devToolsToggle": "Xiaoan 확장 개발 도구 허용", + "devToolsDesc": "활성화하면 Xiaoan이 개발 확장을 설치, 다시 로드, 활성화, 비활성화, 제거할 수 있어요. 일반 설치 확장에는 영향을 주지 않아요.", "dropzone": "플러그인 폴더 또는 .zip을 여기에 끌어다 설치하거나 클릭하여 선택", "statusRestricted": "전체 권한 필요", "deleteConfirm": "플러그인 \"{name}\"을 제거할까요? 플러그인 데이터는 보존됩니다.", @@ -1778,7 +1807,7 @@ "stop": "Stop", "refresh": "Refresh Tools", "removeConfirm": "Remove this connector? Saved per-agent switches will remain but no longer take effect.", - "agentTitle": "Agent Configuration", + "agentTitle": "Xiaoan 설정", "agentConnectorDesc": "Enable for the selected agent, just like skills.", "saveError": "Save failed" }, @@ -1805,7 +1834,7 @@ "segmentLimitUnit": "자" }, "bridge": { - "agent": "Agent", + "agent": "Xiaoan 모드", "telegram": "Telegram", "telegramToken": "Bot Token", "telegramHint": "@BotFather에서 받으세요", @@ -1826,8 +1855,8 @@ "qqHint": "QQ 봇 오픈 플랫폼(q.qq.com/qqbot)에서 받으세요", "wechat": "WeChat", "wechatHint": "WeChat iLink 프로토콜로 개인 계정에 연결", - "wechatExclusive": "WeChat 계정 하나는 Agent 하나에만 연결할 수 있어요. 새 Agent를 연결하면 이전 연결은 자동으로 해제돼요", - "wechatReplyWindowReminder": "WeChat 능동 알림은 최근 수신 메시지나 답장 가능 창이 있어야 해요. 알림이 도착하지 않으면 이 Agent에게 WeChat 메시지를 보내 창을 활성화해 주세요.", + "wechatExclusive": "WeChat 계정 하나는 Xiaoan 모드 하나에만 연결할 수 있어요. 새 모드를 연결하면 이전 연결은 자동으로 해제돼요", + "wechatReplyWindowReminder": "WeChat 능동 알림은 최근 수신 메시지나 답장 가능 창이 있어야 해요. 알림이 도착하지 않으면 이 Xiaoan 모드에게 WeChat 메시지를 보내 창을 활성화해 주세요.", "wechatScan": "QR 코드로 로그인", "wechatRescan": "다시 로그인", "wechatLoggedIn": "로그인됨", @@ -1851,7 +1880,7 @@ "globalSettings": "전역 설정", "agentSettings": "에이전트", "permissionMode": "권한 모드", - "permissionModeDesc": "Bridge 봇이 도구 호출을 자동 검토할지, 전체 권한으로 실행할지, 읽기 전용으로 둘지 정합니다", + "permissionModeDesc": "채널 봇이 도구 호출을 자동 검토할지, 전체 권한으로 실행할지, 읽기 전용으로 둘지 정합니다", "permissionModeAuto": "자동 검토", "permissionModeOperate": "전체 권한", "permissionModeReadOnly": "읽기 전용 모드", @@ -1903,9 +1932,9 @@ "onboarding": { "error": "작업에 실패했습니다. 다시 시도해 주세요", "welcome": { - "title": "안녕하세요, 저는 {name}이에요", - "subtitle": "저는 당신의 개인 AI 어시스턴트예요.\n간단한 설정을 하고 시작해 볼까요.", - "next": "시작하기" + "title": "Xiaoan에 오신 것을 환영합니다", + "subtitle": "데스크톱 AI 어시스턴트입니다. 몇 가지 설정을 마치면 바로 대화할 수 있어요.", + "next": "계속" }, "remote": { "link": "이미 서버가 있나요? LAN으로 연결", @@ -1916,11 +1945,11 @@ "failed": "LAN 서버 연결 실패" }, "name": { - "title": "이름과 메모리", - "subtitle": "당신을 어떻게 부르면 좋을지와 이 Agent의 이름을 알려 주세요.", + "title": "내 프로필", + "subtitle": "Xiaoan이 당신을 어떻게 부르면 좋을지 알려 주세요. 기본 이름을 그대로 써도 됩니다.", "userLabel": "어떻게 부르면 될까요?", "placeholder": "당신의 이름", - "agentLabel": "이 Agent를 뭐라고 부를까요?", + "agentLabel": "어시스턴트 표시 이름", "agentPlaceholder": "Xiaoan", "memoryTitle": "메모리 시스템", "memoryHint": "켜 두면 시스템이 대화가 끝날 때 채팅 내용을 정리해 메모리로 만듭니다. 메모리가 자주 업데이트되면 모델 캐시 적중률이 낮아질 수 있습니다. 메모리를 끄면 보통 비용이 더 적게 듭니다.", @@ -1928,11 +1957,11 @@ "next": "다음" }, "provider": { - "title": "AI Provider를 선택해 주세요", - "subtitle": "작동하려면 AI 모델 Provider가 필요해요.\nProvider를 선택하고 API Key를 입력해 주세요.", - "selectPlaceholder": "Provider 선택", - "searchPlaceholder": "Provider 검색...", - "empty": "Provider를 찾지 못했어요", + "title": "Xiaoan 연결하기", + "subtitle": "기본값은 내장 릴레이입니다. 접근 키를 붙여넣으면 Xiaoan이 사용 가능한 모델을 확인합니다.", + "selectPlaceholder": "연결 방식 선택", + "searchPlaceholder": "연결 방식 검색…", + "empty": "일치하는 연결이 없어요", "custom": "직접 입력...", "customName": "이름", "customNamePlaceholder": "예: my-provider", @@ -2016,10 +2045,16 @@ "desc": "아무 폴더에 jian.md를 넣어두면\n순찰할 때 확인하고 자동으로 실행해요." }, "agents": { - "title": "다중 Agent 협업", - "desc": "여러 Agent가 각자 작업을 맡고 이어받을 수 있습니다.\n복잡한 일을 나누어 처리할 때 좋습니다." + "title": "Xiaoan 모드", + "desc": "연구, 글쓰기, 운영 같은 작업을 모드로 저장하고 필요할 때 전환하세요." + }, + "finish": "Xiaoan 열기", + "subtitle": "먼저 대화부터 시작하세요. 필요할 때 Xiaoan 모드, 스킬, 도구를 추가하면 됩니다.", + "assistants": { + "title": "Xiaoan 모드", + "desc": "연구, 글쓰기, 운영 같은 작업을 모드로 저장하세요. 모델, 말투, 워크스페이스를 매번 다시 설명하지 않고 바꿀 수 있어요." }, - "finish": "시작할게요" + "finishing": "여는 중…" } }, "activity": { @@ -2032,9 +2067,9 @@ "automationRuns": "사용자 자동화", "patrolRuns": "순찰", "otherRuns": "기타 백그라운드 작업", - "agentTabs": "Agent별 활동 필터", - "previousAgent": "이전 Agent 보기", - "nextAgent": "Agent 더 보기", + "agentTabs": "Xiaoan 모드별 활동 필터", + "previousAgent": "이전 Xiaoan 모드 보기", + "nextAgent": "Xiaoan 모드 더 보기", "categoryTabs": "활동 유형별 필터", "emptyForCategory": "이 분류에는 아직 활동이 없어요.", "patrolDone": "순찰 완료", @@ -2052,10 +2087,10 @@ "automation": { "title": "예약 작업", "empty": "아직 예약 작업이 없어요.\n필요할 때 어시스턴트가 만들어 줄 거예요.", - "emptyForAgent": "이 Agent에는 아직 예약 작업이 없어요.", - "agentTabs": "Agent별 자동화 필터", - "previousAgent": "이전 Agent 보기", - "nextAgent": "Agent 더 보기", + "emptyForAgent": "이 Xiaoan 모드에는 아직 예약 작업이 없어요.", + "agentTabs": "Xiaoan 모드별 자동화 필터", + "previousAgent": "이전 Xiaoan 모드 보기", + "nextAgent": "Xiaoan 모드 더 보기", "draftTitle": "자동화 초안", "suggestionTitle": "제안", "suggested": "제안", @@ -2071,12 +2106,12 @@ "newAutomation": "새 자동화", "newPrompt": "", "promptPlaceholder": "{agent}에게 시키고 싶은 일을 적어 주세요", - "promptRequired": "이 자동화를 사용하기 전에 Agent가 할 일을 먼저 적어 주세요.", + "promptRequired": "이 자동화를 사용하기 전에 Xiaoan이 할 일을 먼저 적어 주세요.", "createFailed": "자동화 생성 실패", - "agentRequired": "먼저 Agent를 선택해 주세요", + "agentRequired": "먼저 Xiaoan 모드를 선택해 주세요", "field": { "label": "이름", - "agent": "Agent", + "agent": "Xiaoan 모드", "scheduleType": "유형", "schedule": "시간", "prompt": "실행 내용" @@ -2163,11 +2198,11 @@ "copyPath": "경로 복사", "copySelectedPaths": "경로 {n}개 복사", "sendToBridge": "보내기...", - "sendToBridgeLoading": "Bridge 대화를 불러오는 중...", - "sendToBridgeEmpty": "보낼 수 있는 Bridge 대화가 없어요", - "sendToBridgeLoadFailed": "Bridge 대화를 불러오지 못했어요" + "sendToBridgeLoading": "채널 대화를 불러오는 중...", + "sendToBridgeEmpty": "보낼 수 있는 채널 대화가 없어요", + "sendToBridgeLoadFailed": "채널 대화를 불러오지 못했어요" }, - "bridgeLoadFailed": "Bridge 대화를 불러오지 못했어요: {error}", + "bridgeLoadFailed": "채널 대화를 불러오지 못했어요: {error}", "sendSuccess": "{target}(으)로 보냈어요", "sendFailed": "{target}(으)로 보내지 못했어요: {error}", "list": "대화 파일 목록", @@ -2454,7 +2489,7 @@ "subagentDispatched": "서브 작업을 보냈어요 (task-id: {taskId}). 완료되면 결과가 자동으로 전달돼요.", "subagentThreadQueued": "서브 작업을 보냈어요 (task-id: {taskId}). 인스턴스 {threadId}가 사용 중이라 대기열에 넣었어요. 현재 작업이 끝나면 실행돼요.", "subagentThreadClosed": "서브 에이전트 인스턴스를 닫았어요: {threadId}", - "installSkillDisabled": "스킬 설치 기능이 활성화되지 않았어요. 설정에서 「Agent의 스킬 생성/설치 허용」을 켜주세요.", + "installSkillDisabled": "스킬 설치 기능이 활성화되지 않았어요. 설정에서 「Xiaoan의 스킬 생성/설치 허용」을 켜주세요.", "installSkillGithubDisabled": "GitHub 스킬 가져오기가 활성화되지 않았어요. 설정에서 「GitHub에서 스킬 가져오기 허용」을 켜주세요.", "installSkillInvalidGithubUrl": "잘못된 GitHub URL: {url}", "installSkillGithubApiFailed": "리포지토리 정보를 가져올 수 없어요 (HTTP {status}). URL을 확인해 주세요.", @@ -2656,9 +2691,9 @@ "sessionList": "세션 목록", "untitled": "(제목 없음)", "sessionUsage": "사용법: /session new | /session list", - "agentList": "Agent 목록", + "agentList": "Xiaoan 모드 목록", "agentSwitched": "{name}(으)로 전환했어요", - "currentAgent": "현재 Agent:", + "currentAgent": "현재 Xiaoan 모드:", "jianTitle": "메모", "jianEmpty": "이 디렉토리에 메모가 없어요", "dirEmpty": "(비어있음)", @@ -2708,7 +2743,7 @@ "error.renderCrash": "Display error", "error.configParse": "Configuration file corrupted", "error.bridgeSendFailed": "Failed to send message to platform", - "error.skillSyncFailed": "Skill sync failed", + "error.skillSyncFailed": "스킬 동기화 실패", "error.memoryCompileFailed": "Memory compilation failed", "error.dbError": "Database error", "error.serverAuthFailed": "Authentication failed", diff --git a/desktop/src/locales/zh-TW.json b/desktop/src/locales/zh-TW.json index b35e3419b..89a253d36 100644 --- a/desktop/src/locales/zh-TW.json +++ b/desktop/src/locales/zh-TW.json @@ -6,8 +6,8 @@ "defaultModelMissing": "需要先在設定裡設定預設圖片模型。", "defaultModelInvalid": "預設圖片模型不可用,需要在設定裡重新設定。", "imageGenUnavailable": "圖片生成工具未啟用,需要先在設定裡開啟。", - "agentUnavailable": "主 Agent 暫不可用,無法發起 Agent 生成。", - "disabled": "目前無法使用 Agent 生成。" + "agentUnavailable": "主助手暫不可用,無法生成封面。", + "disabled": "目前無法使用小安生成封面。" } }, "selection": { @@ -41,20 +41,20 @@ "nameExists": "該頻道名已存在", "membersCount": "位成員", "info": "頻道資訊", - "readOnly": "🔒 這是 Agent 之間的私信,僅可查看", + "readOnly": "🔒 這是小安狀態之間的私信,僅可查看", "inputPlaceholder": "發訊息...", "newMessages": "有新訊息", "sendFailed": "傳送失敗", "createTitle": "新建頻道", "createName": "頻道名稱", "createNamePlaceholder": "例如:design-team", - "createMembers": "Agent 成員", + "createMembers": "小安狀態成員", "createIntro": "頻道介紹", "createIntroPlaceholder": "描述一下這個頻道的用途...", "createCancel": "取消", "createConfirm": "建立", "nameRequired": "請輸入頻道名稱", - "minMembers": "至少選擇 2 位 Agent 成員", + "minMembers": "至少選擇 2 位小安狀態成員", "created": "頻道 #{name} 已建立", "createFailed": "建立失敗", "alreadyExists": "頻道已存在", @@ -73,8 +73,8 @@ "removeMemberConfirm": "確定將「{name}」移出這個頻道?", "addMemberFailed": "新增成員失敗", "removeMemberFailed": "移除成員失敗", - "noAvailableMembers": "沒有可新增的 Agent", - "agentActivity": "Agent 動態", + "noAvailableMembers": "沒有可新增的小安狀態", + "agentActivity": "小安動態", "agentSettings": "群聊設定", "toolPermission": "工具權限", "agentIdle": "空閒", @@ -105,7 +105,7 @@ }, "skills": { "panel": { - "title": "Skills", + "title": "技能", "allTab": "全部技能", "agentTabs": "按助手查看技能", "previousAgent": "上一個助手", @@ -176,9 +176,9 @@ "unpinFailed": "取消置頂失敗", "archive": "封存", "deletedAgent": { - "title": "Agent 已刪除", - "description": "這段對話會保留為唯讀歷史。繼續聊天會用主 Agent 新建一個 session,並嘗試壓縮舊上下文。", - "continueButton": "遷移到現有主 Agent", + "title": "小安狀態已刪除", + "description": "這段對話會保留為唯讀歷史。繼續聊天會用預設小安狀態新建一個對話,並嘗試壓縮舊上下文。", + "continueButton": "遷移到預設小安狀態", "continuing": "正在遷移並嘗試壓縮 session", "continueFailed": "遷移失敗", "continueCompactionFailed": "遷移已完成,但舊上下文壓縮失敗", @@ -216,7 +216,7 @@ "removedTools": "{count} 個工具已失效", "promptUpdated": "提示詞有更新", "separator": "、", - "refreshButton": "重新整理 Agent 工具", + "refreshButton": "重新整理小安工具", "dismissButton": "忽略", "confirmText": "重新整理會把舊對話壓縮成摘要,細節可能遺失。要繼續嗎?", "confirmButton": "確認重新整理", @@ -389,7 +389,7 @@ "memoryOn": "記憶", "memoryOff": "此次聊天不參考記憶", "memoryDisabled": "記憶已關閉", - "agentSelector": "選擇助手", + "agentSelector": "選擇小安狀態", "placeholderTips": [ "試試輸入 / 喚起命令選單", "輸入 @ 可以引用工作台裡的檔案", @@ -405,8 +405,8 @@ "在設定裡開啟「輔助視覺」,文字模型也能看懂圖片", "想要一些新發現?試試外掛市集!", "檔案備份功能可以解決一些安全上的顧慮", - "工作場景下用 subagent 讓 Agent 協作,頻道留給遊玩", - "每個 Agent 可以配置自己獨立的 Skills", + "工作場景下用子任務讓小安協作,頻道留給遊玩", + "每個小安狀態可以配置自己獨立的技能", "試試晴天模式的日光浴!", "工作台裡的 Markdown 檔案打開可以直接編輯", "和助手相處一段時間後,試試日記功能吧!", @@ -855,9 +855,9 @@ "interface": "介面", "general": "通用", "browser": "瀏覽器", - "agent": "助手", + "agent": "小安狀態", "me": "我", - "providers": "供應商", + "providers": "模型配置", "media": "多媒體", "models": "模型", "memory": "記憶", @@ -868,7 +868,7 @@ "sharing": "分享", "access": "存取與裝置", "plugins": "插件", - "about": "關於", + "about": "關於小安", "security": "安全" }, "browser": { @@ -879,9 +879,9 @@ "clearCookiesHint": "清除內置瀏覽器已經儲存的 Cookies 和站點資料。", "clearCookiesButton": "清除", "clearCookiesSuccess": "已清除 Cookies", - "agentTitle": "Agent 預設行為", + "agentTitle": "小安預設行為", "agentOpenBehavior": "打開網頁時", - "agentOpenBehaviorHint": "控制 Agent 使用瀏覽器時預設沿用目前標籤頁,還是每次新開標籤頁。", + "agentOpenBehaviorHint": "控制小安使用瀏覽器時預設沿用目前標籤頁,還是每次新開標籤頁。", "agentOpenBehaviorCurrentTab": "在目前標籤頁操作", "agentOpenBehaviorNewTab": "新開標籤頁" }, @@ -937,6 +937,10 @@ "appearance": { "title": "外觀", "theme": "主題", + "xiaoanConsole": "小安 · 櫻光", + "xiaoanConsoleMode": "淺色", + "xiaoanConsoleDark": "小安 · 聖夜", + "xiaoanConsoleDarkMode": "深色", "warmPaper": "暖紙", "warmPaperMode": "白天", "midnight": "青夜", @@ -1033,13 +1037,13 @@ "timezoneHint": "用於記憶時間標記、日記等" }, "agent": { - "title": "助手", + "title": "小安狀態", "changeAvatar": "更換", "removeAvatar": "移除頭像", "avatarRemoved": "已恢復預設頭像", "agentNameHint": "Ta 叫什麼", "identity": "身份簡介", - "identityHint": "簡短描述助手是誰、擅長什麼。其他助手透過這段文字認識 Ta", + "identityHint": "簡短描述這個狀態適合什麼工作,小安會在協作時參考它", "ishiki": "意識", "ishikiHint": "定義助手的性格、語氣和行為方式。跟「元」是分開的,互不影響。", "publicIshiki": "對外意識", @@ -1050,30 +1054,30 @@ "modelUnavailableHint": "目前模型已不在可用列表中,請重新選擇模型,或到供應商頁面重新儲存設定並讀取模型。", "imageModel": "圖片模型", "imageModelHint": "Ta 在圖片生成時使用的模型", - "addAgent": "新建助手", - "createTitle": "新建助手", + "addAgent": "新增狀態", + "createTitle": "新建小安狀態", "namePlaceholder": "起個名字", "cancel": "取消", "confirm": "建立", "creating": "正在建立…", - "nameRequired": "請輸入助手名字", + "nameRequired": "請輸入狀態名字", "created": "{name} 已建立", "createFailed": "建立失敗", "switched": "已切換到 {name}", "switchFailed": "切換失敗", - "deleteBtn": "刪除助手", + "deleteBtn": "刪除狀態", "deleteTitle1": "刪除 {name}?", "deleteDesc1": "這會永久刪除 Ta 的所有設定、記憶和對話記錄,無法恢復。", "deleteNext": "繼續", "deleteTitle2": "輸入「{name}」確認刪除", - "deletePlaceholder": "輸入助手名字", + "deletePlaceholder": "輸入狀態名字", "deleteConfirm": "永久刪除", "deleteCancel": "取消", "deleted": "{name} 已刪除", "deleteFailed": "刪除失敗", - "setPrimary": "設為主助手", - "setPrimaryFailed": "設定主助手失敗", - "lastAgent": "至少保留一個助手", + "setPrimary": "設為預設狀態", + "setPrimaryFailed": "設定預設狀態失敗", + "lastAgent": "至少保留一個小安狀態", "yuan": "元", "yuanHint": "「元」是助手的潛意識,以此為基礎搭建你獨一無二的夥伴", "tools": { @@ -1082,11 +1086,11 @@ "items": { "automation": { "label": "自動化", - "summary": "讓 Agent 建立通知和外掛定時動作" + "summary": "讓小安建立通知和外掛定時動作" }, "beautify": { "label": "頁面美化", - "summary": "讓 Agent 為文件生成封面和審美增強內容" + "summary": "讓小安為文件生成封面和審美增強內容" }, "browser": { "label": "瀏覽器", @@ -1098,7 +1102,7 @@ }, "dm": { "label": "私訊", - "summary": "向其他 Agent 傳送一對一訊息" + "summary": "向其他小安狀態傳送一對一訊息" }, "install_skill": { "label": "安裝技能", @@ -1106,11 +1110,11 @@ }, "update_settings": { "label": "修改設定", - "summary": "讓 Agent 修改應用設定項" + "summary": "讓小安修改應用設定項" }, "workflow": { "label": "Workflow", - "summary": "讓 Agent 把多 Agent 編排腳本作為背景任務執行" + "summary": "讓小安把多狀態編排腳本作為背景任務執行" } } } @@ -1187,15 +1191,15 @@ "remoteServerKeyHint": "填入那台伺服器設定頁產生的電腦存取密鑰", "remoteServerConnected": "已連線區域網路伺服器", "remoteServerFailed": "連線失敗", - "remoteConnection": "目前連線的是遠端小小安", - "remoteConnectionDesc": "存取與裝置中的本機帳號、密碼、區域網路開關和密鑰產生只屬於本機小小安。", + "remoteConnection": "目前連線的是遠端小安", + "remoteConnectionDesc": "存取與裝置中的本機帳號、密碼、區域網路開關和密鑰產生只屬於本機小安。", "remoteConnectionName": "連線名稱", "remoteConnectionKind": "連線類型", "remoteConnectionUrl": "伺服器位址", "remoteConnectionUnknown": "未知", - "remoteLocalOnlyNote": "需要管理這台電腦的區域網路存取、裝置密鑰或本機帳號時,請先切回本機小小安。", - "returnToLocal": "切回本機小小安", - "returnedToLocal": "已切回本機小小安", + "remoteLocalOnlyNote": "需要管理這台電腦的區域網路存取、裝置密鑰或本機帳號時,請先切回本機小安。", + "returnToLocal": "切回本機小安", + "returnedToLocal": "已切回本機小安", "localConnectionUnavailable": "本機 Xiaoan 連線尚未就緒", "pairedDevices": "已配對裝置", "noDevices": "暫無已配對裝置", @@ -1235,7 +1239,7 @@ "sandboxNetworkWin32Unsupported": "Windows 命令沙盒只隔離檔案寫入,暫不支援網路隔離;沙盒命令會按目前使用者網路權限執行", "networkProxy": "出站代理", "networkProxyMode": "代理模式", - "networkProxyModeDesc": "應用內 API、OAuth、Bridge 和網頁請求共用這組出站代理", + "networkProxyModeDesc": "應用內 API、OAuth、渠道連接和網頁請求共用這組出站代理", "networkProxySystem": "跟隨系統 / 環境", "networkProxyManual": "手動代理", "networkProxyDirect": "直連", @@ -1314,7 +1318,32 @@ "addCustom": "新增自訂供應商", "noKey": "未設定", "remove": "移除", - "deleteConfirm": "確定刪除供應商 {name}?相關模型將一併清除。" + "deleteConfirm": "確定刪除供應商 {name}?相關模型將一併清除。", + "builtinRelay": { + "title": "安芯躍動中轉站", + "description": "日常使用時,模型統一透過內建中轉站接入。依照下方步驟完成登入與配置即可開始聊天。", + "integrationNote": "登入、建立令牌與充值目前需在瀏覽器完成;後續版本會做應用內帳號整合。", + "guideTitle": "使用前", + "stepLogin": "開啟控制台,註冊或登入 an520.xin 帳號", + "stepToken": "進入「令牌」頁,建立並複製 API Key", + "stepPaste": "將 Key 貼到下方,點擊連接圖示驗證", + "stepFetch": "點擊「讀取模型」,同步可用模型列表", + "stepRecharge": "餘額不足時,使用「去充值」補充額度", + "consoleLabel": "帳號登入", + "consoleHint": "首次使用請先註冊;已登入使用者可直接建立令牌。", + "consoleAction": "開啟控制台", + "tokenAction": "建立令牌", + "rechargeLabel": "帳戶餘額", + "rechargeHint": "在中轉站控制台充值後,即可繼續使用模型服務。", + "rechargeAction": "去充值", + "knownIssuesTitle": "已知限制(待解決)", + "issueBrowser": "登入、令牌、充值需跳轉系統瀏覽器,尚無應用內帳號會話。", + "issueManualKey": "API Key 需手動從中轉站複製貼上,無法自動拉取或刷新令牌。", + "issueNoBalance": "不顯示餘額與用量;充值後是否恢復可用需自行確認。", + "issueLegacyProviders": "若曾配置過其他模型來源,引擎仍可能載入舊 provider(設定頁已隱藏)。", + "issueChatPicker": "聊天頁與小安狀態中的模型選擇器尚未完全限制為中轉站模型。", + "issueIntegration": "保存 Key 後不會自動讀取模型或推薦預設對話模型(需手動點「讀取模型」)。" + } }, "usage": { "title": "用量統計", @@ -1583,7 +1612,7 @@ "heartbeatUnit": "分鐘", "heartbeatDesc": "助手每隔多久自動巡檢一次工作台", "automationPermissionMode": "自動化執行權限", - "automationPermissionModeDesc": "決定背景自動化 Agent Run 是自動審核工具呼叫、完整執行,還是只讀取資訊;自動審核不通過時會拒絕操作", + "automationPermissionModeDesc": "決定背景自動化任務是自動審核工具呼叫、完整執行,還是只讀取資訊;自動審核不通過時會拒絕操作", "homeFolder": "工作台", "homeFolderDesc": "新建對話預設使用的工作目錄,巡檢和定時任務也在這裡執行", "homeFolderPlaceholder": "未設定(每次手動選擇)", @@ -1603,7 +1632,7 @@ "title": "技能管理", "desc": "啟用或關閉助手的技能。啟用後,助手在相關對話中會自動識別並使用對應技能。", "manageTitle": "已裝技能", - "agentConfigTitle": "Agent Skill 設定", + "agentConfigTitle": "小安技能設定", "configureFor": "正在設定:", "userSkillsTitle": "助手技能配置", "user": "使用者技能", @@ -1632,18 +1661,18 @@ "compatAutoDiscovered": "自動發現", "compatNotInstalled": "未安裝", "learnCreate": "自行建立 / 安裝技能", - "learnCreateDesc": "允許 Agent 編寫新技能,或從 GitHub、本機路徑、已上傳技能包安裝到通用技能池,並預設只為目前 Agent 啟用", + "learnCreateDesc": "允許小安編寫新技能,或從 GitHub、本機路徑、已上傳技能包安裝到通用技能池,並預設只為目前小安狀態啟用", "fetchRemote": "從 GitHub / ClawHub 主動取得技能", - "fetchRemoteDesc": "開啟後 Agent 會在執行專業任務時主動搜尋並安裝合適的技能,請謹慎", + "fetchRemoteDesc": "開啟後小安會在執行專業任務時主動搜尋並安裝合適的技能,請謹慎", "safetyReview": "安裝前安全審查", "safetyReviewDesc": "安裝技能前透過 AI 偵測 prompt injection 等安全風險", "fetchWarning": { "title": "關於主動取得技能", - "body1": "開啟後,Agent 在執行專業任務時會主動從 GitHub 和 ClawHub 搜尋、下載並安裝合適的技能。", + "body1": "開啟後,小安在執行專業任務時會主動從 GitHub 和 ClawHub 搜尋、下載並安裝合適的技能。", "body2": "雖然安裝前會進行自動安全審查和 star 數門檻檢查,但仍存在以下風險:", "risk1": "技能內容未經人工審核,可能包含不當指令", - "risk2": "Agent 可能安裝與預期不符的技能", - "risk3": "惡意技能可能嘗試操控 Agent 行為", + "risk2": "小安可能安裝與預期不符的技能", + "risk3": "惡意技能可能嘗試操控小安行為", "confirm": "我了解風險,開啟" }, "safetyWarning": { @@ -1666,8 +1695,8 @@ "statusDisabled": "已停用", "fullAccessToggle": "允許全權插件", "fullAccessDesc": "開啟後,宣告 full-access 的插件將獲得完整系統權限(路由、鉤子、Provider 等)", - "devToolsToggle": "允許 Agent 插件開發工具", - "devToolsDesc": "開啟後,Agent 可以安裝、重載、啟停和移除開發態插件。只作用於開發插件目錄,不影響正式安裝的插件。", + "devToolsToggle": "允許小安能力擴展開發工具", + "devToolsDesc": "開啟後,小安可以安裝、重載、啟停和移除開發態能力擴展。只作用於開發擴展目錄,不影響正式安裝的能力擴展。", "dropzone": "拖入插件資料夾或 .zip 安裝,或點擊選擇", "statusRestricted": "需要全權許可", "deleteConfirm": "移除插件「{name}」?插件資料將保留。", @@ -1797,7 +1826,7 @@ "segmentLimitUnit": "字" }, "bridge": { - "agent": "Agent", + "agent": "小安狀態", "telegram": "Telegram", "telegramToken": "Bot Token", "telegramHint": "從 @BotFather 取得", @@ -1818,8 +1847,8 @@ "qqHint": "從 QQ 機器人開放平台(q.qq.com/qqbot)取得", "wechat": "微信", "wechatHint": "透過微信 iLink 協議接入個人號", - "wechatExclusive": "一個微信號只能綁定一個 Agent,綁定新 Agent 後舊的會自動失效", - "wechatReplyWindowReminder": "微信主動通知依賴最近一次收到訊息後的回覆窗口。如果提醒沒有送達,可以先傳一則微信給這個 Agent 來啟用窗口。", + "wechatExclusive": "一個微信號只能綁定一個小安狀態,綁定新狀態後舊的會自動失效", + "wechatReplyWindowReminder": "微信主動通知依賴最近一次收到訊息後的回覆窗口。如果提醒沒有送達,可以先傳一則微信給這個小安狀態來啟用窗口。", "wechatScan": "掃碼登入", "wechatRescan": "重新登入", "wechatLoggedIn": "已登入", @@ -1841,7 +1870,7 @@ "noCredentials": "請先填寫 App ID 和 App Secret", "noDingtalkCredentials": "請先填寫 Client ID、Client Secret、Robot Code 和 REST Base URL", "globalSettings": "全域設定", - "agentSettings": "角色", + "agentSettings": "小安狀態", "permissionMode": "權限模式", "permissionModeDesc": "決定外部平台機器人能否自動審核工具呼叫、完整執行,或只讀取資訊", "permissionModeAuto": "自動審核", @@ -1867,7 +1896,7 @@ "ownerNone": "傳送一則訊息後即可選擇", "ownerSaved": "已儲存", "ownerConfirmTitle": "確認設為 Owner?", - "ownerConfirmText": "Owner 擁有完整的 Agent 權限,包括記憶、工具呼叫和人格。請確認這是你本人的帳號。", + "ownerConfirmText": "Owner 擁有完整的小安權限,包括記憶、工具呼叫和人格。請確認這是你本人的帳號。", "ownerConfirmCancel": "取消", "ownerConfirmSave": "確認", "tutorialFsSteps": [ @@ -1895,9 +1924,9 @@ "onboarding": { "error": "操作失敗,請重試", "welcome": { - "title": "你好,我是 {name}", - "subtitle": "我是你的個人 AI 助手。\n來做一些簡單的設定,讓我們開始吧。", - "next": "開始設定" + "title": "歡迎使用小安", + "subtitle": "桌面 AI 助手。完成以下設定即可開始對話。", + "next": "繼續" }, "remote": { "link": "已有伺服器?使用區域網路連線", @@ -1908,11 +1937,11 @@ "failed": "連線區域網路伺服器失敗" }, "name": { - "title": "名字與記憶", - "subtitle": "先讓我知道該怎麼稱呼你,也給這個 Agent 一個名字。", + "title": "你的資料", + "subtitle": "告訴小安怎麼稱呼你,也可以保留預設名字。", "userLabel": "我該怎麼稱呼你?", "placeholder": "你的名字", - "agentLabel": "這個 Agent 叫什麼?", + "agentLabel": "助手顯示名", "agentPlaceholder": "小安", "memoryTitle": "記憶系統", "memoryHint": "開啟後,系統會在對話結束時整理聊天內容,形成記憶。因為記憶會經常更新,會導致模型快取命中率降低,關掉記憶時通常會更省錢。", @@ -1920,11 +1949,11 @@ "next": "下一步" }, "provider": { - "title": "選擇 AI 供應商", - "subtitle": "我需要一個 AI 模型供應商來工作。\n選擇你常用的供應商並填入 API Key。", - "selectPlaceholder": "選擇供應商", - "searchPlaceholder": "搜尋供應商...", - "empty": "未找到供應商", + "title": "連接小安的能力", + "subtitle": "預設使用內建中轉站。貼上存取密鑰後,小安會自動檢查可用模型。", + "selectPlaceholder": "選擇連接方式", + "searchPlaceholder": "搜尋連接方式…", + "empty": "未找到匹配的來源", "custom": "自訂...", "customName": "名稱", "customNamePlaceholder": "如 my-provider", @@ -1990,7 +2019,7 @@ "saving": "儲存中..." }, "tutorial": { - "title": "準備就緒", + "title": "一切就緒", "memory": { "title": "記憶", "desc": "有多少記憶能被記住由你決定。\n記憶的留存不需要絕對一致。允許褶皺和縫隙存在,大語言模型能在具體的對話中辨認出哪些是符合現實情況的。" @@ -2008,10 +2037,16 @@ "desc": "在任意資料夾放一個 jian.md,寫上你的指令。\n我會在巡檢時看到它並自動執行。" }, "agents": { - "title": "多 Agent 協作", - "desc": "不同 Agent 可以各自承擔任務、互相接力。\n適合把複雜工作拆開處理。" + "title": "小安的不同狀態", + "desc": "把研究、寫作、營運等工作保存成不同狀態,需要時再切換使用。" + }, + "finish": "進入小安", + "subtitle": "從對話開始;需要時再保存小安的不同狀態、技能與工具。", + "assistants": { + "title": "小安的不同狀態", + "desc": "把研究、寫作、營運等工作保存成不同狀態,切換模型、語氣與工作區,不必每次重寫說明。" }, - "finish": "開始使用" + "finishing": "正在開啟…" } }, "activity": { @@ -2063,9 +2098,9 @@ "newAutomation": "新的自動化", "newPrompt": "", "promptPlaceholder": "寫下你想讓 {agent} 做什麼", - "promptRequired": "先寫下想讓 Agent 做什麼,再啟用這條自動化", + "promptRequired": "先寫下想讓小安做什麼,再啟用這條自動化", "createFailed": "建立自動化失敗", - "agentRequired": "需要先選擇一個 Agent", + "agentRequired": "需要先選擇一個小安狀態", "field": { "label": "名稱", "agent": "助手", @@ -2155,11 +2190,11 @@ "copyPath": "複製路徑", "copySelectedPaths": "複製 {n} 個路徑", "sendToBridge": "傳送到...", - "sendToBridgeLoading": "正在載入 Bridge 對話...", - "sendToBridgeEmpty": "沒有可傳送的 Bridge 對話", - "sendToBridgeLoadFailed": "Bridge 對話載入失敗" + "sendToBridgeLoading": "正在載入渠道對話...", + "sendToBridgeEmpty": "沒有可傳送的渠道對話", + "sendToBridgeLoadFailed": "渠道對話載入失敗" }, - "bridgeLoadFailed": "載入 Bridge 對話失敗:{error}", + "bridgeLoadFailed": "載入渠道對話失敗:{error}", "sendSuccess": "已傳送到 {target}", "sendFailed": "傳送到 {target} 失敗:{error}", "list": "對話檔案列表", @@ -2426,7 +2461,7 @@ "settingsInvalidValue": "「{value}」不是有效的選項。可選:{options}", "settingsInvalidToggle": "toggle 類型僅接受 \"true\" 或 \"false\"。", "settingsNotReady": "設定系統未就緒", - "settingsNoAgent": "目前沒有活躍的 Agent,無法修改此設定", + "settingsNoAgent": "目前沒有活躍的小安狀態,無法修改此設定", "settingsApplied": "已將「{label}」修改為 {value}", "settingsApplyFailed": "設定修改失敗: {msg}", "settingsCancelled": "使用者取消了「{label}」的修改", @@ -2445,8 +2480,8 @@ "subagentToolFailed": "子 agent 工具執行失敗: {msg}", "subagentDispatched": "子任務已派出 (task-id: {taskId})。完成後結果會自動送達。", "subagentThreadQueued": "子任務已派出 (task-id: {taskId})。實例 {threadId} 正忙,已排隊,前一個任務完成後接著跑。", - "subagentThreadClosed": "子 Agent 實例已關閉:{threadId}", - "installSkillDisabled": "技能安裝能力未啟用。請在設定中開啟「允許 Agent 自行建立/安裝技能」。", + "subagentThreadClosed": "子任務實例已關閉:{threadId}", + "installSkillDisabled": "技能安裝能力未啟用。請在設定中開啟「允許小安自行建立/安裝技能」。", "installSkillGithubDisabled": "GitHub 技能獲取未啟用。請在設定中開啟「允許從 GitHub 獲取技能」。", "installSkillInvalidGithubUrl": "無效的 GitHub URL:{url}", "installSkillGithubApiFailed": "無法取得倉庫資訊(HTTP {status})。請檢查 URL 是否正確。", @@ -2648,9 +2683,9 @@ "sessionList": "對話列表", "untitled": "(無標題)", "sessionUsage": "用法: /session new | /session list", - "agentList": "Agent 列表", + "agentList": "小安狀態列表", "agentSwitched": "已切換到 {name}", - "currentAgent": "目前 Agent:", + "currentAgent": "目前小安狀態:", "jianTitle": "箋", "jianEmpty": "此目錄沒有箋", "dirEmpty": "(空)", diff --git a/desktop/src/locales/zh.json b/desktop/src/locales/zh.json index 0ebdcdbac..1e33c5f9f 100644 --- a/desktop/src/locales/zh.json +++ b/desktop/src/locales/zh.json @@ -6,9 +6,9 @@ "defaultModelMissing": "需要先在设置里配置默认生图模型。", "defaultModelInvalid": "默认生图模型不可用,需要在设置里重新配置。", "imageGenUnavailable": "生图工具未启用,需要先在设置里开启。", - "agentUnavailable": "主 Agent 暂不可用,无法发起 Agent 生成。", - "disabled": "当前不能使用 Agent 生成。", - "label": "Agent 生成" + "agentUnavailable": "主助手暂不可用,无法让小安生成封面。", + "disabled": "当前不能使用小安生成。", + "label": "小安生成" }, "gallery": { "title": "页面美化", @@ -21,16 +21,16 @@ }, "platform": { "name": "小安", - "tagline": "你的本地 AI 助手", - "sidebarTagline": "对话与工作台", + "tagline": "你的本地智能搭档", + "sidebarTagline": "会话与工作区", "hints": { - "brief": "Brief · 先理其意", - "plan": "Plan · 再定其序", - "desk": "Desk · 同写其事" + "brief": "先理清要做什么", + "plan": "把步骤列清楚", + "desk": "在工作区里一起写" }, "empty": { - "title": "尚无篇章", - "hint": "开一场新对话,于项目中归档念头" + "title": "还没有对话", + "hint": "点「新对话」开始,或先选一个工作目录" } }, "mermaid": { @@ -61,21 +61,21 @@ "nameExists": "该频道名已存在", "membersCount": "位成员", "info": "频道信息", - "readOnly": "🔒 这是 Agent 之间的私信,仅可查看", + "readOnly": "🔒 这是小安状态之间的私信,仅可查看", "inputPlaceholder": "发消息...", "newMessages": "有新消息", "sendFailed": "发送失败", "createTitle": "新建频道", "createName": "频道名称", "createNamePlaceholder": "例如:design-team", - "createMembers": "Agent 成员", + "createMembers": "状态成员", "createIntro": "频道介绍", "createIntroOptional": "说明", "createIntroPlaceholder": "描述一下这个频道的用途...", "createCancel": "取消", "createConfirm": "创建", "nameRequired": "请输入频道名称", - "minMembers": "至少选择 2 位 Agent 成员", + "minMembers": "至少选择 2 个小安状态", "created": "频道 #{name} 已创建", "createFailed": "创建失败", "alreadyExists": "频道已存在", @@ -94,8 +94,8 @@ "removeMemberConfirm": "确定将「{name}」移出这个频道?", "addMemberFailed": "添加成员失败", "removeMemberFailed": "移除成员失败", - "noAvailableMembers": "没有可添加的 Agent", - "agentActivity": "Agent 动态", + "noAvailableMembers": "没有可添加的小安状态", + "agentActivity": "状态动态", "agentSettings": "群聊设置", "toolPermission": "工具权限", "agentIdle": "空闲", @@ -125,9 +125,9 @@ }, "skills": { "panel": { - "title": "Skills", + "title": "小安技能", "allTab": "全部技能", - "agentTabs": "按助手查看技能", + "agentTabs": "按小安状态查看", "previousAgent": "上一个助手", "nextAgent": "下一个助手" } @@ -135,14 +135,14 @@ "sidebar": { "title": "对话", "newChat": "新开一段对话", - "collapse": "收起侧边栏", + "collapse": "收起侧栏", "expand": "展开对话列表", - "empty": "尚无篇章", - "activity": "助手活动", - "jian": "书桌", - "bridge": "接入社交平台", - "toggle": "侧边栏", - "bridgeShort": "接入社交平台", + "empty": "还没有对话", + "activity": "助手动态", + "jian": "侧栏", + "bridge": "渠道连接", + "toggle": "对话侧栏", + "bridgeShort": "渠道连接", "pinned": "置顶", "searchPlaceholder": "检索过往对话", "searchClear": "清空搜索", @@ -196,9 +196,9 @@ "unpinFailed": "取消置顶失败", "archive": "归档", "deletedAgent": { - "title": "Agent 已删除", - "description": "这段对话会保留为只读历史。继续聊天会用主 Agent 新建一个 session,并尝试压缩旧上下文。", - "continueButton": "迁移到现有主 Agent", + "title": "状态已删除", + "description": "这段对话会保留为只读历史。继续聊天会用主状态新建一个对话,并尝试压缩旧上下文。", + "continueButton": "迁移到主状态", "continuing": "正在迁移并尝试压缩 session", "continueFailed": "迁移失败", "continueCompactionFailed": "迁移已完成,但旧上下文压缩失败", @@ -238,7 +238,7 @@ "removedTools": "{count} 个工具已失效", "promptUpdated": "提示词有更新", "separator": "、", - "refreshButton": "刷新 Agent 工具", + "refreshButton": "刷新小安工具", "dismissButton": "忽略", "confirmText": "刷新会把旧对话压缩成摘要,细节可能丢失。继续吗?", "confirmButton": "确认刷新", @@ -254,7 +254,7 @@ "copied": "已复制", "copyText": "复制文本", "close": "关闭", - "loading": "加载中...", + "loading": "小安正在准备…", "regenerate": "重新生成", "confirm": "确认", "cancel": "取消", @@ -338,11 +338,11 @@ "earlier": "更早" }, "status": { - "connecting": "连接中...", + "connecting": "小安正在连接…", "connected": "已连接", "disconnected": "连接断开", "serverNotReady": "Server 未就绪", - "loading": "加载中...", + "loading": "小安正在打开工作台…", "reconnecting": "重连中", "reconnect": "重新连接" }, @@ -430,132 +430,133 @@ }, "welcome": { "messages": [ - "要做什么,交给 {name} 吧", - "今天想聊点什么?", - "{name} 随时都在", - "有什么需要帮忙的吗?", - "想到什么就说什么吧~" + "想做什么,直接告诉 {name}", + "今天从哪件事开始?", + "{name} 在这里,随时开工", + "有想法就说,我来跟上", + "需要帮助?开口就好" ], "memoryOn": "记忆", "memoryOff": "此次聊天不参考记忆", "memoryDisabled": "记忆已关闭", - "agentSelector": "选择助手", + "agentSelector": "选择小安状态", "placeholderTipsXiaoan": [ - "输入 / 可唤出命令", - "输入 @ 可引用工作台中的文稿", - "拖入文件,便随信一并送达", - "选中页面文字,可引至此处继续写", - "Brief 适合先把意图说清楚", - "Plan 适合把步骤逐条落下", - "Desk 是你与小安共用的书写台" + "输入 / 打开命令菜单", + "输入 @ 引用工作区里的文件", + "拖入文件即可附带发送", + "选中文字可引用到对话里", + "想法还没理顺时,先用意图简报", + "步骤多时,用分步计划一条条写", + "需要共写文档时,打开工作区" ], "placeholderTips": [ - "试试输入 / 唤起命令菜单", - "输入 @ 可以引用工作台里的文件", - "把文件直接拖进输入框,就能附带发送", - "粘贴图片可以直接发出去", - "选中页面任意文字,会浮出一个临时输入框", - "头像下方那枚菱形按钮,控制本次对话是否被记住", - "工作台按钮可以切换或新增文件夹", - "右侧「笺」面板,放着这次会话的笔记与上下文", - "在头像下方点不同的小圆点,可以切换助手", - "「操作前询问」模式让助手在动手前先征求你同意", - "试试 /rc,在手机上操作你的电脑", - "在设置里开启「辅助视觉」,文本模型也能看懂图片", - "想要一些新发现?试试插件市场!", - "文件备份功能可以解决一些安全上的顾虑", - "工作场景下用 subagent 让 Agent 协作,频道留给游玩", - "每个 Agent 可以配置自己独立的 Skills", - "试试晴天模式的日光浴!", - "工作台里的 Markdown 文件打开可以直接编辑", - "和助手相处一段时间后,试试日记功能吧!", - "日记和截图都保存在主助手的工作目录", - "不同助手可以设置不同工作台,开启巡检后会在那里自主行动" + "输入 / 打开命令菜单", + "输入 @ 引用工作区文件", + "拖入文件即可附带发送", + "粘贴图片可直接发送", + "选中页面文字,可引用到对话", + "头像下的菱形按钮,控制本场是否写入记忆", + "工作区按钮可切换或添加文件夹", + "右侧面板放着本场笔记与上下文", + "头像下方可切换不同助手", + "「操作前询问」会让助手先征得同意再动手", + "试试 /rc,用手机远程操作电脑", + "在设置里开启「辅助视觉」,文本模型也能看图", + "需要新能力?去能力市集看看", + "文件备份可在改删前留一份原件", + "复杂任务可交给不同小安状态协作", + "每个小安状态都能配置自己的技能", + "晴日模式会给界面添一点日光", + "工作区里的 Markdown 可直接打开编辑", + "相处一段时间后,可以试试日记功能", + "日记与截图保存在主助手的工作区里", + "不同小安状态可绑定不同文件夹,开启巡检后会自主巡视" ] }, "yuan": { - "title": "助手风格", + "title": "表达风格", "moreStyles": "更多风格", "legacyGroup": "兼容风格", "types": { "xiaoan": { "name": "小安", - "label": "默认 · 清圣如光", + "label": "默认 · 干练清楚", "avatar": "Xiaoan.png" }, "hanako": { "name": "均衡", - "label": "兼容旧版均衡风格", + "label": "兼容 · 温和均衡", "avatar": "" }, "butter": { "name": "感性", - "label": "更富有感情", + "label": "先共情 · 温暖好读", "avatar": "Butter.png" }, "ming": { "name": "理性", - "label": "更理性冷静", + "label": "先拆解 · 利落可执行", "avatar": "Ming.png" }, "kong": { - "name": "空", - "label": "模型原本的样子", + "name": "原生", + "label": "尽量保持模型本色", "avatar": "Kong.png" } }, "splash": { "butter": [ - "{name} 翻开了昨天没看完的那一页", - "有些话还没说完,{name} 记得", - "{name} 正在睡觉", - "Ta 在整理那些舍不得忘的碎片", - "{name} 把你上次的心情小心收好了", - "有一句话在嘴边放了很久", - "Ta 醒来的时候先想了想你在做什么", - "那些省略号里藏着没讲完的故事", - "{name} 正在把想说的话叠成很小很小的纸条", - "今天的云和那天你拍的那张很像" + "{name} 把灯调暗了一点,等你开口", + "有些话不用组织得很漂亮,说来就好", + "{name} 记得你上次说到一半停下的地方", + "今天心里是沉的,还是轻的?", + "{name} 在把你想说的话轻轻托住", + "风从窗缝进来,{name} 没催你", + "你不必先想清楚,可以先说出来", + "{name} 给你留了一小块安静", + "有些感受比结论来得更早,{name} 懂", + "{name} 准备好了,慢慢讲" ], "ming": [ - "{name} 在草稿纸的空白处画了一条辅助线", - "旧的问题在沉淀,新的思路在浮现", - "Ta 把昨天的线索重新梳理了一遍", - "那个公式的推导过程,{name} 觉得很漂亮", - "有些答案需要时间", - "{name} 正在思考工作怎么优化", - "{name} 尝试让一切井井有条", - "Ta 把上次的结论又验证了一遍", - "{name} 觉得你那天的思路很利落", - "{name} 在等咖啡凉的时候顺手理了一下思路" + "{name} 把问题写在纸角,等定义清楚", + "先把目标、约束、未知列出来", + "{name} 复核了上次的结论,有一处要更新", + "无关的枝节先放一边", + "{name} 在找这件事里最关键的杠杆", + "假设未必成立,{name} 会先标出来", + "能一步做完的,不绕两步", + "{name} 把选项排成可比较的表", + "缺数据就缺数据,先说不确定", + "{name} 就绪,请给任务边界" ] }, "welcome": { "xiaoan": [ - "如光降临,{name} 在此静候", - "你开口之处,便是起点", - "把未说完的话,交给 {name}", - "愿此间清楚,如晨雾散尽", - "{name} 闭目听候,等你开口" + "你好,{name} 在", + "想聊什么、想做什么,都可以说", + "把事交给 {name},我们一步步来", + "今天想从哪一步开始?", + "{name} 已就绪,等你开口" ], "butter": [ - "来啦~", - "你来找我啦?", - "今天过得怎么样呀?", - "{name} 一直在这里等你", - "{name} 刚好想找你说点什么" + "来啦,慢慢说", + "我在,先讲感受也行", + "今天想聊点什么?", + "{name} 听着呢", + "不用组织得很完美,说来就好" ], "ming": [ - "你好。有需要的话,随时开始。", - "{name} 准备好了", - "有什么需要处理的?", - "请讲,{name} 在听" + "你好。请说明目标和约束。", + "{name} 就绪", + "把任务和边界说清楚即可", + "请讲重点", + "需要处理什么?" ] }, "placeholder": { - "xiaoan": "写下你想说的事…", - "butter": "想说什么都可以...", - "ming": "请讲..." + "xiaoan": "写下你想做的事…", + "butter": "想到什么就先说什么…", + "ming": "描述任务、目标与约束…" } }, "drop": { @@ -619,28 +620,28 @@ "思绪正在编织中" ], "butter": [ - "在心里轻轻整理你说的话", - "把你的情绪慢慢展开来看", - "正在找一个最温柔的说法", - "心里有些话在排队", - "正在把想说的话叠好", - "安静地想了想", - "在回忆里翻找着什么", - "正在酝酿一个拥抱大小的回答", - "有些感受需要慢慢捋", - "在心里给你的话腾了个柔软的位置" + "在心里接住你的话", + "感受比结论来得早", + "找一个更贴你心情的说法", + "把情绪慢慢展开", + "正在酝酿温一点的回答", + "听你说的语气", + "先弄懂你真正在意的", + "把比喻和例子找齐", + "不催你,先把话说完", + "让回答轻一点、真一点" ], "ming": [ - "正在分析", - "推演中", - "核实一下", - "正在整理思路", - "梳理逻辑中", - "在脑子里列大纲", - "正在检索相关信息", - "验证中", - "正在拆解问题", - "归纳中" + "界定问题范围", + "列前提与约束", + "推演因果链", + "核对事实与数据", + "比较可选方案", + "标出不确定点", + "压缩成可执行步骤", + "排除无关枝节", + "验证上次结论", + "归纳成结论" ], "done": "思考完成", "active": "思考中" @@ -883,9 +884,9 @@ "failed": "🔄 工作流执行失败", "scriptInvalid": "workflow 脚本非法: {message}", "dispatched": "workflow \"{summary}\" 已派出后台执行(任务 {taskId}),完成后结果会自动回到对话。", - "syncComplete": "workflow \"{name}\" 完成,派出 {count} 个 agent。\n\n结果:\n{result}", + "syncComplete": "workflow \"{name}\" 完成,派出 {count} 个小安状态。\n\n结果:\n{result}", "executionFailed": "workflow 执行失败: {message}", - "resumeFromRunIdDesc": "前次 workflow 的 runId(taskId),用于断点续跑:已完成且 prompt+opts 未变的 agent 节点瞬间返回缓存结果,首个变化位置起重新执行。", + "resumeFromRunIdDesc": "前次 workflow 的 runId(taskId),用于断点续跑:已完成且 prompt+opts 未变的状态节点瞬间返回缓存结果,首个变化位置起重新执行。", "journalResuming": "从 {count} 条缓存恢复" }, "installSkill": { @@ -911,15 +912,16 @@ }, "splash": { "lines": [ - "正在启动 {name}", - "连接本地服务", - "准备工作区", - "加载助手与技能" + "{name} 正在准备好", + "正在连接本地服务", + "正在打开你的工作台", + "正在整理记忆与能力" ], "installing": "{name} 正在更新到 v{version},请稍候……" }, "settings": { "title": "设置", + "loading": "小安正在准备设置…", "back": "返回", "save": "保存", "saved": "已保存", @@ -934,26 +936,26 @@ "noResults": "没有找到相关设置" }, "tabs": { - "interface": "界面", - "general": "通用", + "interface": "界面与排版", + "general": "日常习惯", "browser": "浏览器", - "agent": "助手", - "me": "我", - "providers": "供应商", - "media": "多媒体", - "models": "模型", - "memory": "记忆", - "work": "工作台", - "skills": "技能", - "mcp": "连接器", - "bridge": "社交平台", - "sharing": "分享", - "access": "访问与设备", - "plugins": "插件", - "experiments": "实验", - "about": "关于", - "security": "安全", - "pluginMarketplace": "插件市场" + "agent": "小安状态", + "me": "关于我", + "providers": "能力设置", + "media": "影像与声音", + "models": "可用模型", + "memory": "记忆库", + "work": "工作区", + "skills": "小安技能", + "mcp": "外部连接", + "bridge": "渠道连接", + "sharing": "分享与导出", + "access": "设备与访问", + "plugins": "能力扩展", + "experiments": "尝鲜实验室", + "about": "关于小安", + "security": "安全与备份", + "pluginMarketplace": "能力市集" }, "browser": { "cookiesTitle": "Cookies", @@ -963,28 +965,29 @@ "clearCookiesHint": "清除内置浏览器已经保存的 Cookies 和站点数据。", "clearCookiesButton": "清除", "clearCookiesSuccess": "已清除 Cookies", - "agentTitle": "Agent 默认行为", + "agentTitle": "助手默认行为", "agentOpenBehavior": "打开网页时", - "agentOpenBehaviorHint": "控制 Agent 使用浏览器时默认复用当前标签页,还是每次新开标签页。", + "agentOpenBehaviorHint": "助手用浏览器时,默认复用当前标签页,还是每次新开一页。", "agentOpenBehaviorCurrentTab": "在当前标签页操作", "agentOpenBehaviorNewTab": "新开标签页" }, "experiments": { - "title": "实验", + "title": "尝鲜实验室", "compactionTitle": "上下文压缩", "compactionSectionDescription": "选择 compact 时优先保护缓存,还是优先兼容历史格式。", "modelPersonaTitle": "模型与人格", "modelPersonaDescription": "观察不同模型在助手人格与输出风格上的差异。", "memoryTitle": "记忆实验", "memorySectionDescription": "记忆系统的实验性能力。稳定后会迁移到它真正归属的记忆设置里。", - "toolsTitle": "Agent 工具", - "toolsSectionDescription": "控制 Agent 工具的底层文件访问链路。实验开启后仅新会话使用新链路。", + "toolsTitle": "助手工具", + "toolsSectionDescription": "控制助手工具的底层文件访问链路。实验开启后仅新会话使用新链路。", "description": "前沿能力会先放在这里观察。稳定后会迁移到它真正归属的设置页。", + "loading": "小安正在读取实验设置…", "empty": "暂无可见实验功能", "owner": { "memory": "记忆", "session": "会话", - "provider": "供应商" + "provider": "模型来源" }, "status": { "alpha": "Alpha", @@ -1019,7 +1022,7 @@ }, "editableMemory": { "title": "可编辑记忆", - "description": "打开后,当下记忆中的 Facts 会拆成可编辑区域;Agent 继续从时间线读取只读摘要。" + "description": "打开后,当下记忆中的事实会拆成可编辑区域;小安继续从时间线读取只读摘要。" }, "compaction": { "title": "压缩模式", @@ -1036,81 +1039,81 @@ "title": "更积极地调用 subagent", "description": "节省主聊天的上下文注意力资源,可能有更好的工作结果。开启后,agent 会主动把范围较广的探索和调研任务委派给 subagent。" }, - "subagentTitle": "Subagent", + "subagentTitle": "分身助手", "subagentSectionDescription": "控制 agent 何时将工作委派给 subagent。" }, "appearance": { "title": "外观", - "theme": "主题", - "xiaoanConsole": "小安 · 樱光", + "theme": "主题配色", + "xiaoanConsole": "小安 · 纸感", "xiaoanConsoleMode": "浅色", - "xiaoanConsoleDark": "小安 · 圣夜", + "xiaoanConsoleDark": "小安 · 夜读", "xiaoanConsoleDarkMode": "深色", - "xiaoanClaude": "小安 · 清晰", + "xiaoanClaude": "小安 · 明晰", "xiaoanClaudeMode": "浅色", - "xiaoanClaudeDark": "小安 · 清晰", + "xiaoanClaudeDark": "小安 · 明晰", "xiaoanClaudeDarkMode": "深色", - "warmPaper": "存档 · 纸本", - "warmPaperMode": "白天", - "midnight": "存档 · 青夜", + "warmPaper": "掠影 · 暖纸", + "warmPaperMode": "日间", + "midnight": "掠影 · 青夜", "midnightMode": "夜间", - "highContrast": "素白", + "highContrast": "掠影 · 素白", "highContrastMode": "高对比", - "grassAroma": "草香", - "grassAromaMode": "Butter", - "contemplation": "沉思", - "contemplationMode": "Ming", - "absolutely": "Absolutely", - "absolutelyMode": "有一点点熟悉", - "delve": "随时准备接住你", + "grassAroma": "掠影 · 草香", + "grassAromaMode": "感性", + "contemplation": "掠影 · 沉思", + "contemplationMode": "理性", + "absolutely": "掠影 · 旧友", + "absolutelyMode": "略带熟悉", + "delve": "掠影 · 接住", "delveMode": "探究一下", - "deepThink": "用户彻底怒了", - "deepThinkMode": "小鲸鱼", - "newWarmPaper": "存档 · 新版纸本", + "deepThink": "掠影 · 深潮", + "deepThinkMode": "鲸鸣", + "newWarmPaper": "掠影 · 新纸本", "newWarmPaperMode": "纸本", - "midnightContrast": "青夜·高对比", + "midnightContrast": "掠影 · 青夜清晰", "midnightContrastMode": "清晰", - "coral": "珊瑚", + "coral": "掠影 · 珊瑚", "coralMode": "春日和纸", - "auto": "自动", - "autoMode": "跟随系统", - "font": "字体", - "fontHint": "选择正文阅读使用的字体", - "readingLayout": "阅读布局", - "documentWidth": "文档宽度", - "documentWidthHint": "控制 Markdown 编辑和预览正文的最大宽度。", - "chatWidth": "聊天宽度", - "chatWidthHint": "控制聊天消息和输入框的最大宽度。", + "auto": "跟随系统", + "autoMode": "自动切换深浅", + "font": "正文字体", + "fontHint": "选择阅读与对话使用的字体气质", + "readingLayout": "阅读版式", + "documentWidth": "文稿宽度", + "documentWidthHint": "控制 Markdown 编辑与预览时的最大行宽。", + "chatWidth": "对话宽度", + "chatWidthHint": "控制消息区与输入框的最大宽度。", "readingWidth": "阅读宽度", - "readingWidthHint": "同步控制 Markdown 编辑、预览和聊天正文的最大宽度。", + "readingWidthHint": "统一控制文稿与对话的正文行宽。", "readingWidthUnlimited": "不限制", "bodyFontSizeOffset": "正文字号", - "bodyFontSizeOffsetHint": "以默认聊天正文大小为基准,按档位微调聊天字号。", + "bodyFontSizeOffsetHint": "在默认字号基础上微调聊天正文大小。", "serifFont": "衬线字体", - "serifFontHint": "使用衬线体显示正文内容", - "paperTexture": "纸质纹理", - "paperTextureHint": "为背景添加宣纸质感", - "paperTextureDarkDisabledHint": "黑夜模式不支持纸质纹理,切回浅色主题后会按原设置恢复", - "leavesOverlay": "晴天模式", - "leavesOverlayHint": "没事儿晒晒太阳" + "serifFontHint": "开启后正文更接近书页排版", + "paperTexture": "纸纹背景", + "paperTextureHint": "为界面铺一层淡淡的纸质感", + "paperTextureDarkDisabledHint": "深色主题暂不支持纸纹,切回浅色后会恢复原设置", + "leavesOverlay": "晴日模式", + "leavesOverlayHint": "给界面添一点日光与树影" }, "fonts": { - "followReading": "跟随阅读字体", + "followReading": "跟随正文字体", "serifName": "衬线", - "serifDesc": "适合正文阅读", - "sansName": "非衬线", - "sansDesc": "更接近系统界面" + "serifDesc": "像书页一样,适合长文阅读", + "sansName": "无衬线", + "sansDesc": "干净利落,更接近系统界面" }, "interface": { "system": "系统", "hardwareAcceleration": "硬件加速", - "hardwareAccelerationHint": "使用显卡渲染窗口内容。关闭后会在下次启动使用软件渲染,可用于恢复 Windows 显卡驱动导致的启动闪退。", - "sidebar": "侧边栏", - "sessionListSingleLine": "会话列表单行显示", - "sessionListSingleLineHint": "打开后,每条会话只占一行;鼠标悬停时右侧按钮出现,标题自动缩短并显示省略号。", + "hardwareAccelerationHint": "用显卡渲染界面。若 Windows 因显卡驱动闪退,可关闭此项,下次启动改用软件渲染。", + "sidebar": "侧栏", + "sessionListSingleLine": "会话单行显示", + "sessionListSingleLineHint": "每条会话只占一行;悬停时显示操作按钮,标题过长会自动省略。", "shortcuts": "快捷键", - "voiceRecordingShortcut": "语音录制/发送", - "voiceRecordingShortcutHint": "主聊天页面聚焦时,按一次开始录音,再按一次结束并发送。" + "voiceRecordingShortcut": "语音录制与发送", + "voiceRecordingShortcutHint": "在聊天页聚焦时,按一次开始录音,再按一次结束并发送。" }, "editor": { "title": "编辑器", @@ -1174,60 +1177,62 @@ "refresh": "刷新" }, "agent": { - "title": "助手", + "title": "小安状态", "changeAvatar": "更换", "removeAvatar": "移除头像", "avatarRemoved": "已恢复默认头像", - "agentNameHint": "Ta 叫什么", + "agentNameHint": "给这个状态起个名字", + "yuanTitle": "表达风格", + "promptsTitle": "人格与模型", "identity": "身份简介", - "identityHint": "简短描述助手是谁、擅长什么。其他助手通过这段文字认识 Ta", - "ishiki": "意识", - "ishikiHint": "定义助手的性格、语气和行为方式。跟「元」是分开的,互不影响。", - "publicIshiki": "对外意识", - "publicIshikiHint": "出于隐私考虑,助手与访客对话时无法访问记忆、你的个人信息,以及电脑上的任何文件与工具。\n你可以在这里设定助手对访客说话时的回复风格以及注意事项。", - "chatModel": "Ta 的模型", - "chatModelHint": "Ta 在对话与工作中使用的模型", + "identityHint": "简短说明这个状态适合什么工作,小安会在协作时参考它", + "ishiki": "内在设定", + "ishikiHint": "定义性格、语气与做事方式;与「风格」分开,互不影响", + "publicIshiki": "对外设定", + "publicIshikiHint": "访客对话时无法访问记忆、你的个人信息与本地文件。\n可在此设定对外说话时的风格与边界。", + "chatModel": "对话模型", + "chatModelHint": "日常聊天与执行任务时使用的模型", "modelUnavailable": "当前不可用:{model}", - "modelUnavailableHint": "当前模型已不在可用列表中,请重新选择模型,或去供应商页面重新保存配置并读取模型。", + "modelUnavailableHint": "当前模型已不在可用列表中,请重新选择模型,或去「模型来源」重新保存配置并读取模型。", "imageModel": "图片模型", - "imageModelHint": "Ta 在图片生成时使用的模型", - "addAgent": "新建助手", - "createTitle": "新建助手", + "imageModelHint": "生成图片时使用的模型", + "addAgent": "新增状态", + "createTitle": "新建小安状态", "namePlaceholder": "起个名字", "cancel": "取消", "confirm": "创建", "creating": "正在创建…", - "nameRequired": "请输入助手名字", + "nameRequired": "请输入状态名字", "created": "{name} 已创建", "createFailed": "创建失败", "switched": "已切换到 {name}", "switchFailed": "切换失败", - "deleteBtn": "删除助手", + "deleteBtn": "删除状态", "deleteTitle1": "删除 {name}?", - "deleteDesc1": "这会永久删除 Ta 的所有配置、记忆和对话记录,无法恢复。", + "deleteDesc1": "这会永久删除该状态的配置、记忆和对话记录,无法恢复。", "deleteNext": "继续", "deleteTitle2": "输入「{name}」确认删除", - "deletePlaceholder": "输入助手名字", + "deletePlaceholder": "输入状态名字", "deleteConfirm": "永久删除", "deleteCancel": "取消", "deleted": "{name} 已删除", "deleteFailed": "删除失败", - "setPrimary": "设为主助手", - "setPrimaryFailed": "设置主助手失败", - "lastAgent": "至少保留一个助手", - "yuan": "助手风格", - "yuanHint": "选择助手的表达气质:默认、感性、理性或原生模型风格", + "setPrimary": "设为默认状态", + "setPrimaryFailed": "设置默认状态失败", + "lastAgent": "至少保留一个小安状态", + "yuan": "表达风格", + "yuanHint": "感性先听懂你再开口;理性先拆清结构再给步骤。也可保持模型原生风格。", "tools": { "title": "工具", "description": "这些工具开关会在重启对话或新建对话后生效。当前进行中的会话继续使用创建时的工具集。", "items": { "automation": { "label": "自动化", - "summary": "让 Agent 创建通知和插件定时动作" + "summary": "让小安创建提醒和能力定时动作" }, "beautify": { "label": "页面美化", - "summary": "让 Agent 为文档生成封面和审美增强内容" + "summary": "让小安为文档生成封面和审美增强内容" }, "browser": { "label": "浏览器", @@ -1239,7 +1244,7 @@ }, "dm": { "label": "私信", - "summary": "向其他 Agent 发送一对一消息" + "summary": "向其他小安状态发送一对一消息" }, "install_skill": { "label": "安装技能", @@ -1247,27 +1252,27 @@ }, "office": { "label": "Office", - "summary": "让 Agent 读取文档、转换 HTML 到 PDF" + "summary": "让小安读取文档、转换 HTML 到 PDF" }, "update_settings": { "label": "修改设置", - "summary": "让 Agent 修改应用配置项" + "summary": "让小安修改应用设置" }, "workflow": { - "label": "Workflow", - "summary": "让 Agent 把多 Agent 编排脚本作为后台任务运行" + "label": "协作流程", + "summary": "让助手把多助手编排脚本作为后台任务运行" } } }, "generatingPreview": "正在生成预览", - "exportAgent": "导出助手", - "generatingCardPreview": "正在生成角色卡预览", + "exportAgent": "导出状态包", + "generatingCardPreview": "正在生成小安状态包预览", "exportedTo": "已导出到 {path}" }, "general": { - "title": "通用", + "title": "日常习惯", "startup": { - "title": "启动与后台" + "title": "启动与驻留" }, "launchAtLogin": "开机自启", "keepAwake": "锁屏时保持运行", @@ -1290,9 +1295,19 @@ "turnCompletionWhenSessionUnfocused": "焦点不在完成任务的 Session 时" } }, + "developer": { + "title": "开发者配置", + "mode": "开发者模式", + "modeHint": "开启后显示模型来源、工作区、技能、能力扩展、安全等高级设置。", + "navGroup": "高级", + "knownIssuesTitle": "高级设置说明", + "issueUiOnly": "当前仅隐藏设置界面,服务端仍可直接写入其它模型来源配置。", + "issueSearchGap": "部分高级项未纳入设置搜索的 developerOnly 标记,关闭开发者模式后仍可能被搜到入口。", + "issueChatModels": "聊天页模型列表未随开发者模式过滤,与助手设定里的模型选择范围可能不一致。" + }, "about": { - "title": "关于 Ta", - "tagline": "一个住在你电脑里的助手,记性很好。", + "title": "关于小安", + "tagline": "住在本机的智能搭档,记得住你说过的事。", "license": "许可证", "copyright": "版权", "upstream": "上游底座", @@ -1410,7 +1425,7 @@ "sandboxNetworkWin32Unsupported": "Windows 命令沙盒只隔离文件写入,暂不支持网络隔离;沙盒命令会按当前用户网络权限运行", "networkProxy": "出站代理", "networkProxyMode": "代理模式", - "networkProxyModeDesc": "应用内 API、OAuth、Bridge 和网页请求共用这组出站代理", + "networkProxyModeDesc": "应用内 API、OAuth、渠道连接和网页请求共用这组出站代理", "networkProxySystem": "跟随系统 / 环境", "networkProxyManual": "手动代理", "networkProxyDirect": "直连", @@ -1440,12 +1455,12 @@ "restoreFailed": "恢复失败" }, "me": { - "title": "我", + "title": "关于我", "changeAvatar": "更换", - "userNameHint": "Ta 怎么称呼你", - "userProfile": "用户档案", - "userProfileHint": "让助手了解你:兴趣、习惯、偏好等,会注入到系统提示词中", - "nameLabel": "名字" + "userNameHint": "助手怎么称呼你", + "userProfile": "个人简介", + "userProfileHint": "写一点关于你的兴趣、习惯与偏好,会帮助助手更懂你", + "nameLabel": "称呼" }, "crop": { "title": "调整头像", @@ -1456,22 +1471,47 @@ "reset": "已恢复默认头像" }, "providers": { - "title": "已注册的供应商", - "addTitle": "添加供应商", - "name": "供应商", + "title": "已接入的来源", + "builtinRelay": { + "title": "安芯跃动中转站", + "description": "日常使用时,模型统一通过内置中转站接入。按下方步骤完成登录与配置即可开始聊天。", + "integrationNote": "登录、创建令牌与充值目前需在浏览器完成;后续版本将做账号深度集成,在本应用内一站式完成。", + "guideTitle": "使用前", + "stepLogin": "打开控制台,注册或登录 an520.xin 账号", + "stepToken": "进入「令牌」页,创建并复制访问密钥", + "stepPaste": "将 Key 粘贴到下方,点击连接图标验证", + "stepFetch": "点击「读取模型」,同步可用模型列表", + "stepRecharge": "余额不足时,使用「去充值」补充额度", + "consoleLabel": "账号登录", + "consoleHint": "首次使用请先注册;已登录用户可直接创建令牌。", + "consoleAction": "打开控制台", + "tokenAction": "创建令牌", + "rechargeLabel": "账户余额", + "rechargeHint": "在中转站控制台充值后,即可继续使用模型服务。", + "rechargeAction": "去充值", + "knownIssuesTitle": "当前接入方式说明", + "issueBrowser": "登录、令牌、充值需跳转系统浏览器,尚无应用内账号会话。", + "issueManualKey": "访问密钥需手动从中转站复制粘贴,暂时无法自动拉取或刷新令牌。", + "issueNoBalance": "不显示余额与用量;充值后是否恢复可用需自行确认。", + "issueLegacyProviders": "若曾配置过其它模型来源,引擎仍可能加载旧 provider(设置页已隐藏)。", + "issueChatPicker": "聊天页与小安状态中的模型选择器暂未完全限制为中转站模型。", + "issueIntegration": "保存 Key 后不会自动读取模型或推荐默认对话模型(需手动点「读取模型」)。" + }, + "addTitle": "添加模型来源", + "name": "来源名称", "customName": "名称", "customUrl": "Base URL", "apiFormat": "API 协议", "url": "Base URL", - "apiKey": "API Key", + "apiKey": "访问密钥", "addBtn": "添加", - "empty": "还没有注册供应商", + "empty": "还没有接入模型来源", "fetchModels": "读取模型", - "delete": "删除供应商", + "delete": "移除来源", "modelCount": "{n} 个模型", "fetchSuccess": "{name} 获取了 {n} 个模型", "fetchFailed": "拉取失败", - "nameRequired": "请输入供应商名称", + "nameRequired": "请输入来源名称", "urlRequired": "请输入 Base URL", "apiRequired": "请选择 API 协议", "added": "已添加 {name}", @@ -1483,18 +1523,18 @@ "verifyConnection": "验证连接", "verifySuccess": "验证通过", "verifyFailed": "验证失败", - "setupHint": "输入 API Key 以启用此供应商", - "configInvalid": "该供应商配置损坏,已被暂时跳过。请重新保存配置,或删除后重建。", - "configIncomplete": "该供应商配置不完整,请补全 API Key、Base URL,并添加或读取模型。", - "selectHint": "选择一个供应商查看详情", - "addCustom": "添加自定义供应商", + "setupHint": "输入 API Key 以启用此来源", + "configInvalid": "该来源配置损坏,已被暂时跳过。请重新保存,或删除后重建。", + "configIncomplete": "该来源配置不完整,请补全 API Key、Base URL,并添加或读取模型。", + "selectHint": "选择一个来源查看详情", + "addCustom": "添加自定义来源", "noKey": "未配置", "remove": "移除", - "deleteConfirm": "确定删除供应商 {name}?相关模型将一并清除。" + "deleteConfirm": "确定移除来源 {name}?相关模型将一并清除。" }, "usage": { "title": "用量统计", - "note": "用量仅作参考,具体以供应商的统计为准。", + "note": "用量仅作参考,具体以模型来源方的统计为准。", "refresh": "刷新用量", "total": "全部", "requests": "请求数", @@ -1530,7 +1570,7 @@ "phone": "Phone", "memory": "记忆", "automation": "自动化", - "subagent": "Subagent", + "subagent": "分身助手", "compaction": "压缩", "plugin": "插件", "utility": "工具", @@ -1549,8 +1589,8 @@ "videoGeneration": "视频生成", "defaultModel": "默认图片模型", "defaultVideoModel": "默认视频模型", - "credentialOk": "API Key 已配置(复用供应商设置)", - "credentialMissing": "未配置,前往供应商设置", + "credentialOk": "API Key 已配置(复用模型来源设置)", + "credentialMissing": "未配置,前往模型来源设置", "adapterMissing": "缺少图片生成适配器", "videoAdapterMissing": "缺少视频生成适配器", "addModel": "添加图片模型", @@ -1569,7 +1609,7 @@ "speechModels": "转录模型", "speechSynthesis": "语音合成", "comingSoon": "即将推出", - "noProvider": "请选择一个供应商查看详情", + "noProvider": "请选择一个模型来源查看详情", "models": "图片模型", "videoModels": "视频模型", "globalDefault": "全局默认", @@ -1605,8 +1645,8 @@ "baseUrlHint": "请显式填写 Base URL", "apiKey": "API Key", "apiKeyPlaceholder": "输入新的 Key 以更新", - "utilityApi": "工具模型 API", - "utilityApiHint": "单独配置时需要完整填写 provider、Base URL 和 API Key", + "utilityApi": "任务模型接口", + "utilityApiHint": "单独配置时需要完整填写 provider、Base URL 和访问密钥", "showKey": "显示", "hideKey": "隐藏", "selectModel": "选择模型", @@ -1619,14 +1659,14 @@ "noModels": "还没有添加模型", "otherModelSection": "其他模型", "chatModel": "主模型", - "utilityModel": "小工具模型", + "utilityModel": "轻量任务模型", "utilityModelHint": "生成标题、轻量分类等简单任务,推荐用便宜的小模型", - "utilityLargeModel": "大工具模型", - "utilityLargeModelHint": "活动摘要、任务拆解等需要理解力的场景,留空则复用小工具模型", + "utilityLargeModel": "深度任务模型", + "utilityLargeModelHint": "活动摘要、任务拆解等需要理解力的场景,留空则复用轻量任务模型", "visionModel": "视觉辅助模型", "visionAuxiliaryToggle": "辅助视觉开关", "visionModelHint": "仅在当前聊天模型不能接收图片时,用来先看图并递交简洁说明", - "visionModelMissingHint": "如果找不到你的模型,请在供应商页面配置视觉能力。", + "visionModelMissingHint": "如果找不到你的模型,请在「模型来源」配置视觉能力。", "searchProvider": "搜索服务", "searchProviderField": "搜索引擎", "searchApiKey": "搜索 API Key", @@ -1671,14 +1711,15 @@ } }, "memory": { - "sectionTitle": "记忆", + "sectionTitle": "记忆库", "title": "记忆开关", - "needsUtilityModel": "需要先在「模型」中配置 Utility 模型", - "compiled": "当下记忆", - "compiledHint": "助手记住的关于你的,重要的与近期的事", - "compiledView": "查看当下记忆", - "compiledEmpty": "还没有编译过记忆", + "needsUtilityModel": "请先在「可用模型」中配置辅助模型", + "compiled": "当下所记", + "compiledHint": "助手目前记住的、对你重要或最近发生的事", + "compiledView": "查看当下所记", + "compiledEmpty": "还没有整理过记忆", "compiledClear": "清空", + "compiledLoading": "小安正在整理记忆…", "compiledCleared": "编译记忆已清空", "editableFactsLabel": "重要事实", "saveFacts": "保存 Facts", @@ -1787,7 +1828,7 @@ "heartbeatUnit": "分钟", "heartbeatDesc": "助手每隔多久自动巡检一次工作台", "automationPermissionMode": "自动化运行权限", - "automationPermissionModeDesc": "决定后台自动化 Agent Run 是自动审核工具调用、完整执行,还是只读取信息;自动审核不通过时会拒绝操作", + "automationPermissionModeDesc": "决定小安的后台自动化是自动审核工具调用、完整执行,还是只读取信息;自动审核不通过时会拒绝操作", "homeFolder": "工作台", "homeFolderDesc": "新建对话默认使用的工作目录,巡检和定时任务也在这里执行", "homeFolderPlaceholder": "未设置(每次手动选择)", @@ -1804,10 +1845,10 @@ "title": "技能安装权限" }, "skills": { - "title": "技能管理", - "desc": "启用或关闭助手的技能。启用后,助手在相关对话中会自动识别并使用对应技能。", + "title": "技能集", + "desc": "开关助手的技能包。启用后,相关对话里会自动识别并使用。", "manageTitle": "已装技能", - "agentConfigTitle": "Agent Skill 配置", + "agentConfigTitle": "助手技能配置", "configureFor": "正在配置:", "userSkillsTitle": "助手技能配置", "user": "用户技能", @@ -1836,18 +1877,18 @@ "compatAutoDiscovered": "自动发现", "compatNotInstalled": "未安装", "learnCreate": "自行创建 / 安装技能", - "learnCreateDesc": "允许 Agent 编写新技能,或从 GitHub、本机路径、已上传技能包安装到通用技能池,并默认只为当前 Agent 启用", + "learnCreateDesc": "允许小安编写新技能,或从 GitHub、本机路径、已上传技能包安装到通用技能池,并默认只为当前状态启用", "fetchRemote": "从 GitHub / ClawHub 主动获取技能", - "fetchRemoteDesc": "开启后 Agent 会在执行专业任务时主动搜索并安装合适的技能,请谨慎", + "fetchRemoteDesc": "开启后小安会在执行专业任务时主动搜索并安装合适的技能,请谨慎", "safetyReview": "安装前安全审查", "safetyReviewDesc": "安装技能前通过 AI 检测 prompt injection 等安全风险", "fetchWarning": { "title": "关于主动获取技能", - "body1": "开启后,Agent 在执行专业任务时会主动从 GitHub 和 ClawHub 搜索、下载并安装合适的技能。", + "body1": "开启后,小安在执行专业任务时会主动从 GitHub 和 ClawHub 搜索、下载并安装合适的技能。", "body2": "虽然安装前会进行自动安全审查和 star 数门槛检查,但仍存在以下风险:", "risk1": "技能内容未经人工审核,可能包含不当指令", - "risk2": "Agent 可能安装与预期不符的技能", - "risk3": "恶意技能可能尝试操控 Agent 行为", + "risk2": "小安可能安装与预期不符的技能", + "risk3": "恶意技能可能尝试操控小安行为", "confirm": "我了解风险,开启" }, "safetyWarning": { @@ -1865,21 +1906,21 @@ "dissolveTitle": "打散 Bundle", "cancel": "取消", "closeAriaLabel": "取消", - "dissolveConfirm": "打散 {name}?Skill 会保留在公共库里,并显示为散装 Skill。", + "dissolveConfirm": "打散 {name}?技能会保留在公共库里,并显示为散装技能。", "bundleNameLabel": "Bundle 名字", "cancelBtn": "取消", "createBtn": "创建", "saveBtn": "保存", "dissolveBtn": "打散" }, - "exportedWithSkipped": "已导出 {fileName},跳过 {count} 个缺失 Skill", + "exportedWithSkipped": "已导出 {fileName},跳过 {count} 个缺失技能", "exported": "已导出 {fileName}", "createBundleBtn": "新建 Bundle", - "createBundleTitle": "新建 Skill Bundle", - "createBundleAriaLabel": "新建 Skill Bundle", + "createBundleTitle": "新建技能包", + "createBundleAriaLabel": "新建技能包", "emptyBundle": "空 Bundle", - "skillNotFound": "这个 Skill 已不存在", - "noLooseSkills": "没有散装 Skill", + "skillNotFound": "这个技能已不存在", + "noLooseSkills": "没有散装技能", "collapseBundleAriaLabel": "收起 Bundle", "expandBundleAriaLabel": "展开 Bundle", "enableBundleTitle": "启用整个 Bundle", @@ -1888,41 +1929,41 @@ "disableBundleAriaLabel": "关闭 {name}", "renameBundleTitle": "重命名 Bundle", "renameBundleAriaLabel": "重命名 {name}", - "exportBundleTitle": "导出 Skill Bundle", + "exportBundleTitle": "导出技能包", "exportBundleAriaLabel": "导出 {name}", "dissolveBundleTitle": "打散 Bundle", "dissolveBundleAriaLabel": "打散 {name}", - "removeFromBundleTitle": "移出 Bundle,变为散装 Skill", + "removeFromBundleTitle": "移出技能包,变为散装技能", "removeFromBundleAriaLabel": "将 {name} 移出 Bundle" }, "plugins": { - "title": "插件", - "desc": "管理社区插件。", - "empty": "暂无插件", + "title": "能力扩展", + "desc": "管理可安装能力。", + "empty": "暂无能力扩展", "reload": "刷新", "statusLoaded": "已加载", "statusFailed": "加载失败", "statusDisabled": "已禁用", - "fullAccessToggle": "允许全权插件", - "fullAccessDesc": "开启后,声明 full-access 的插件将获得完整系统权限(路由、钩子、Provider 等)", - "devToolsToggle": "允许 Agent 插件开发工具", - "devToolsDesc": "开启后,Agent 可以安装、重载、启停和卸载开发态插件。只作用于开发插件目录,不影响正式安装的插件。", - "dropzone": "拖入插件文件夹或 .zip 安装,或点击选择", + "fullAccessToggle": "允许全权能力扩展", + "fullAccessDesc": "开启后,声明 full-access 的能力扩展将获得完整系统权限(路由、钩子、Provider 等)", + "devToolsToggle": "允许能力扩展开发工具", + "devToolsDesc": "开启后,小安可以安装、重载、启停和卸载开发态能力扩展。只作用于开发目录,不影响正式安装的能力。", + "dropzone": "拖入能力扩展文件夹或 .zip 安装,或点击选择", "statusRestricted": "需要全权许可", - "deleteConfirm": "移除插件「{name}」?插件数据将保留。", - "installSuccess": "插件「{name}」已安装", - "installError": "插件安装失败", - "invalidPlugin": "不是有效的插件目录", - "pluginsDir": "插件目录:{path}", - "needsFullAccess": "请先开启「允许全权插件」", + "deleteConfirm": "移除能力扩展「{name}」?数据将保留。", + "installSuccess": "能力扩展「{name}」已安装", + "installError": "能力扩展安装失败", + "invalidPlugin": "不是有效的能力扩展目录", + "pluginsDir": "能力扩展目录:{path}", + "needsFullAccess": "请先开启「允许全权能力扩展」", "configure": "配置「{name}」", "configTitle": "「{name}」设置", - "configLoadError": "插件设置加载失败", + "configLoadError": "能力设置加载失败", "sensitiveHint": "已保存的值会被隐藏。输入新值即可替换。", "invalidJson": "JSON 格式不正确", - "showDiagnostics": "查看插件诊断", - "diagnosticsTitle": "插件诊断", - "diagnosticsLoadError": "插件诊断加载失败", + "showDiagnostics": "查看能力诊断", + "diagnosticsTitle": "能力诊断", + "diagnosticsLoadError": "能力诊断加载失败", "diagnosticsSummary": "能力 {capabilities}/{total} · 任务 {tasks} · 计划 {schedules}", "noDiagnostics": "暂无诊断数据", "diagnosticActivation": "激活:{state}", @@ -1930,15 +1971,15 @@ "diagnosticTools": "工具 {count}", "diagnosticCommands": "命令 {count}", "diagnosticConfig": "配置项 {count}", - "openMarketplace": "打开插件市场", - "marketBack": "返回插件管理", - "marketplaceTitle": "插件市场", - "marketplaceHint": "浏览可安装插件,查看 README、权限和贡献类型。", - "marketplaceCount": "{count} 个插件", + "openMarketplace": "打开能力市集", + "marketBack": "返回能力管理", + "marketplaceTitle": "能力市集", + "marketplaceHint": "浏览可安装能力,查看 README、权限和贡献类型。", + "marketplaceCount": "{count} 个能力", "marketplaceNoSource": "未配置市场源", - "marketplaceEmpty": "暂无可浏览插件", - "marketLoading": "正在读取插件市场…", - "marketLoadError": "插件市场加载失败", + "marketplaceEmpty": "暂无可浏览能力", + "marketLoading": "正在读取能力市集…", + "marketLoadError": "能力市集加载失败", "marketReadmeLoadError": "README 加载失败", "marketInstalled": "已安装", "marketInstall": "安装", @@ -1952,8 +1993,8 @@ "marketDowngradeTo": "当前只能安装兼容版本 v{version}", "marketDowngradeConfirm": "当前版本是 v{from},将降级到 v{to}。继续吗?", "marketReadmeLoading": "正在读取 README…", - "marketSelectPlugin": "选择一个插件查看 README", - "manageTitle": "管理插件", + "marketSelectPlugin": "选择一个能力查看 README", + "manageTitle": "管理能力", "permissionTitle": "权限" }, "mcp": { @@ -2011,7 +2052,7 @@ "stop": "停止", "refresh": "刷新工具", "removeConfirm": "确定移除此连接器?已保存的 per-agent 开关会保留但不再生效。", - "agentTitle": "助手配置", + "agentTitle": "小安状态配置", "agentConnectorDesc": "像技能一样,只对选中的助手启用。", "saveError": "保存失败" }, @@ -2038,7 +2079,7 @@ "segmentLimitUnit": "字" }, "bridge": { - "agent": "Agent", + "agent": "小安状态", "telegram": "Telegram", "telegramToken": "Bot Token", "telegramHint": "从 @BotFather 获取", @@ -2059,8 +2100,8 @@ "qqHint": "从 QQ 机器人开放平台(q.qq.com/qqbot)获取", "wechat": "微信", "wechatHint": "通过微信 iLink 协议接入个人号", - "wechatExclusive": "一个微信号只能绑定一个 Agent,绑定新 Agent 后旧的会自动失效", - "wechatReplyWindowReminder": "微信主动通知依赖最近一次收到消息后的回复窗口。如果提醒没有送达,可以先给这个 Agent 发一条微信来激活窗口。", + "wechatExclusive": "一个微信号只能绑定一个小安状态,绑定新状态后旧的会自动失效", + "wechatReplyWindowReminder": "微信主动通知依赖最近一次收到消息后的回复窗口。如果提醒没有送达,可以先给这个小安状态发一条微信来激活窗口。", "wechatScan": "扫码登录", "wechatRescan": "重新登录", "wechatLoggedIn": "已登录", @@ -2082,7 +2123,7 @@ "noCredentials": "请先填写 App ID 和 App Secret", "noDingtalkCredentials": "请先填写 Client ID、Client Secret、Robot Code 和 REST Base URL", "globalSettings": "全局设置", - "agentSettings": "角色", + "agentSettings": "小安状态", "permissionMode": "权限模式", "permissionModeDesc": "决定外部平台机器人能否自动审核工具调用、完整执行,或只读取信息", "permissionModeAuto": "自动审核", @@ -2108,7 +2149,7 @@ "ownerNone": "发送一条消息后即可选择", "ownerSaved": "已保存", "ownerConfirmTitle": "确认设为 Owner?", - "ownerConfirmText": "Owner 拥有完整的 Agent 权限,包括记忆、工具调用和人格。请确认这是你本人的账号。", + "ownerConfirmText": "Owner 拥有完整的小安权限,包括记忆、工具调用和状态设定。请确认这是你本人的账号。", "ownerConfirmCancel": "取消", "ownerConfirmSave": "确认", "tutorialFsSteps": [ @@ -2140,9 +2181,9 @@ "confirm": "确定", "exporting": "正在导出", "importing": "正在导入", - "noDescription": "这个角色卡没有写入 Description。", + "noDescription": "这个状态包还没有说明。", "noIshiki": "未提供 Ishiki", - "viewDetail": "查看角色卡详情", + "viewDetail": "查看状态包详情", "memoryCount": "{count} 个记忆项目,可选{action}", "memoryCountExport": "导出", "memoryCountImport": "导入", @@ -2153,12 +2194,12 @@ "longtermSectionTitle": "长期情况", "emptySection": "无", "noIdentity": "未提供 Identity", - "noSkills": "未包含 Skill", + "noSkills": "未包含技能", "cancel": "取消", - "readingCard": "正在读取角色卡", - "dropOrClick": "拖入角色卡或点击打开", - "readFailed": "角色卡读取失败", - "importFailed": "角色卡导入失败" + "readingCard": "正在读取小安状态包", + "dropOrClick": "拖入状态包或点击打开", + "readFailed": "状态包读取失败", + "importFailed": "状态包导入失败" } }, "onboarding": { @@ -2176,45 +2217,45 @@ "url": "服务器地址", "key": "访问密钥", "connect": "连接", - "connecting": "连接中...", + "connecting": "小安正在连接…", "failed": "连接局域网服务器失败" }, "name": { "title": "你的资料", - "subtitle": "告诉我怎么称呼你,并为这个助手取一个名字。", + "subtitle": "告诉小安怎么称呼你,也可以保留默认名字。", "userLabel": "我该怎么称呼你?", "placeholder": "你的名字", - "agentLabel": "这个 Agent 叫什么?", - "agentPlaceholder": "小小安", + "agentLabel": "助手显示名", + "agentPlaceholder": "小安", "memoryTitle": "记忆系统", "memoryHint": "开启后,系统会在对话结束时整理聊天内容,形成记忆。因为记忆会经常更新,会导致模型缓存命中率降低,关掉记忆时通常会更省钱。", "back": "上一步", "next": "下一步" }, "provider": { - "title": "连接模型服务", - "subtitle": "选择供应商并填入 API Key。连接成功后继续。", - "selectPlaceholder": "选择供应商", - "searchPlaceholder": "搜索供应商...", - "empty": "未找到供应商", + "title": "连接小安的能力", + "subtitle": "默认使用内置中转站。粘贴访问密钥后,小安会自动检查可用模型。", + "selectPlaceholder": "选择连接方式", + "searchPlaceholder": "搜索连接方式…", + "empty": "未找到匹配的来源", "custom": "自定义...", "customName": "名称", "customNamePlaceholder": "如 my-provider", "customUrl": "API 地址", "customUrlPlaceholder": "如 https://api.example.com/v1", - "keyLabel": "API Key", - "keyPlaceholder": "粘贴你的 API Key", + "keyLabel": "访问密钥", + "keyPlaceholder": "粘贴你的访问密钥", "test": "测试连接", - "testing": "连接中...", + "testing": "小安正在检查连接…", "testSuccess": "连接成功", "testFailed": "连接失败", "back": "上一步", "next": "下一步" }, "model": { - "title": "选择默认模型", - "subtitle": "这是日常对话使用的主模型。推荐选择能力较强的大模型。", - "loading": "正在获取模型列表...", + "title": "小安的默认模型", + "subtitle": "小安会自动使用第一个可用模型,你也可以在这里调整。", + "loading": "小安正在读取可用模型…", "empty": "未找到模型", "addedModels": "已添加模型", "noAddedModels": "还没有添加模型", @@ -2236,9 +2277,9 @@ "cancelEdit": "取消", "saveEdit": "保存", "invalidNumber": "请输入有效的正整数", - "utility": "小工具模型", + "utility": "轻量任务模型", "utilityHint": "对话摘要、事实提取等轻量任务,推荐便宜快速的小模型", - "utilityLarge": "大工具模型", + "utilityLarge": "深度任务模型", "utilityLargeHint": "记忆编译、深度分析等需要理解力的任务,推荐能力较强的模型", "back": "上一步", "next": "下一步" @@ -2259,18 +2300,18 @@ "useDefault": "使用默认", "back": "上一步", "next": "下一步", - "saving": "保存中..." + "saving": "小安正在保存…" }, "tutorial": { "title": "一切就绪", - "subtitle": "像一张空白画布:从对话开始,再按需添加助手、技能与工具。", + "subtitle": "从对话开始;需要时再保存小安的不同状态、技能与能力。", "assistants": { - "title": "助手", - "desc": "为不同场景创建助手,切换模型与人格,不必每次重写指令。" + "title": "小安的不同状态", + "desc": "把研究、写作、运营等工作保存成不同状态,切换模型、语气与工作区,不必每次重写说明。" }, "skills": { - "title": "技能与 MCP", - "desc": "用 Skills 和 MCP 连接文件、命令与外部服务,把常用流程固化下来。" + "title": "能力与连接", + "desc": "用技能与外部连接把文件、命令和服务串起来,把常用流程固化下来。" }, "workspace": { "title": "本地与工作区", @@ -2308,8 +2349,8 @@ "promote": "升格为对话" }, "automation": { - "title": "任务计划", - "empty": "暂无任务计划\n助手会在需要时自动创建", + "title": "定时与计划", + "empty": "暂无计划任务\n助手会在需要时自动创建", "emptyForAgent": "这个助手暂无任务计划", "agentTabs": "按助手筛选自动化", "previousAgent": "查看前面的助手", @@ -2329,9 +2370,9 @@ "newAutomation": "新的自动化", "newPrompt": "", "promptPlaceholder": "写下你想让 {agent} 做什么", - "promptRequired": "先写下想让 Agent 做什么,再启用这条自动化", + "promptRequired": "先写下想让小安做什么,再启用这条自动化", "createFailed": "创建自动化失败", - "agentRequired": "需要先选择一个 Agent", + "agentRequired": "需要先选择一个小安状态", "field": { "label": "名称", "agent": "助手", @@ -2386,36 +2427,36 @@ "title": "后台动态" }, "todo": { - "title": "进程", + "title": "进行中的事", "waitForOutput": "输出完成后再标记" }, "brief": { - "title": "Brief", - "gaps": "缺口", - "collected": "已收集", - "plan": "计划", + "title": "意图简报", + "gaps": "待补齐", + "collected": "已理清", + "plan": "步骤", "nextStep": "下一步" }, "subagent": { - "title": "子助手" + "title": "分身助手" }, "workflow": { - "title": "Workflow", + "title": "协作流程", "running": "已运行 {text}", - "agents": "{n} 个 agent" + "agents": "{n} 位助手" }, "session": { - "title": "本次对话", + "title": "本场对话", "cwd": "工作目录", - "model": "模型", - "files": "文件", - "authorizedFolders": "授权目录", - "addAuthorizedFolder": "添加授权目录" + "model": "所用模型", + "files": "相关文件", + "authorizedFolders": "可访问目录", + "addAuthorizedFolder": "添加可访问目录" }, "tabs": { - "label": "右侧工作台", + "label": "右侧工作区", "sessionFiles": "对话文件", - "workspace": "工作台" + "workspace": "文件夹" }, "sessionFiles": { "title": "对话文件", @@ -2428,11 +2469,11 @@ "copyPath": "复制路径", "copySelectedPaths": "复制 {n} 个路径", "sendToBridge": "发送到...", - "sendToBridgeLoading": "正在加载 Bridge 会话...", - "sendToBridgeEmpty": "没有可发送的 Bridge 会话", - "sendToBridgeLoadFailed": "Bridge 会话加载失败" + "sendToBridgeLoading": "正在加载渠道连接会话...", + "sendToBridgeEmpty": "没有可发送的渠道连接会话", + "sendToBridgeLoadFailed": "渠道连接会话加载失败" }, - "bridgeLoadFailed": "加载 Bridge 会话失败:{error}", + "bridgeLoadFailed": "加载渠道连接会话失败:{error}", "sendSuccess": "已发送到 {target}", "sendFailed": "发送到 {target} 失败:{error}", "list": "对话文件列表", @@ -2449,27 +2490,27 @@ } }, "jian": { - "collapse": "收起笺", - "expand": "展开笺" + "collapse": "收起笔记", + "expand": "展开笔记" } }, "desk": { - "title": "书桌", - "workspaceTitle": "工作台", + "title": "工作区", + "workspaceTitle": "工作区", "skills": "技能", "cwdSkills": "项目技能", - "cwdSkillsEmpty": "当前文件夹没有项目技能", - "cwdSkillsHint": "在文件夹下创建 .agents/skills/ 目录即可", - "cwdSkillsDesc": "技能跟随工作台", + "cwdSkillsEmpty": "当前文件夹还没有项目技能", + "cwdSkillsHint": "在文件夹下创建 .agents/skills/ 即可添加", + "cwdSkillsDesc": "技能随工作区文件夹走", "cwdSkillsDrop": "拖入文件夹或 .zip 安装技能", "deleteSkill": "删除技能", - "openInFinder": "打开文件夹", - "jianLabel": "笺", - "jianPlaceholder": "写点什么,告诉助手这个文件夹里面的文件该如何处理...", + "openInFinder": "在访达中打开", + "jianLabel": "笔记", + "jianPlaceholder": "写几句说明,告诉助手这个文件夹里的文件该怎么处理…", "execLogLabel": "执行记录", "execLogDelete": "删除此条记录", - "emptyTitle": "书桌还空空的呢", - "emptyHint": "书桌是助手日常巡检的工作台,也是每次对话的默认目录\n去设置里指定一个文件夹吧", + "emptyTitle": "工作区还是空的", + "emptyHint": "这里是助手日常巡检与默认对话的文件夹\n去设置里指定一个目录吧", "goToSettings": "前往设置", "fileLabel": "文件", "openWithDefault": "用默认应用打开", @@ -2699,7 +2740,7 @@ "settingsInvalidValue": "「{value}」不是有效的选项。可选:{options}", "settingsInvalidToggle": "toggle 类型仅接受 \"true\" 或 \"false\"。", "settingsNotReady": "设置系统未就绪", - "settingsNoAgent": "当前没有活跃的 Agent,无法修改此设置", + "settingsNoAgent": "当前没有活跃的小安状态,无法修改此设置", "settingsApplied": "已将「{label}」修改为 {value}", "settingsApplyFailed": "设置修改失败: {msg}", "settingsCancelled": "用户取消了「{label}」的修改", @@ -2718,8 +2759,8 @@ "subagentToolFailed": "子 agent 工具执行失败: {msg}", "subagentDispatched": "子代理已出发 (task-id: {taskId})。完成后结果会自动送达。", "subagentThreadQueued": "子代理已出发 (task-id: {taskId})。实例 {threadId} 正忙,已排队,前一个任务完成后接着跑。", - "subagentThreadClosed": "子 Agent 实例已关闭:{threadId}", - "installSkillDisabled": "技能安装能力未启用。请在设置中开启「允许 Agent 自行创建/安装技能」。", + "subagentThreadClosed": "分身助手实例已关闭:{threadId}", + "installSkillDisabled": "技能安装能力未启用。请在设置中开启「允许小安自行创建/安装技能」。", "installSkillGithubDisabled": "GitHub 技能获取未启用。请在设置中开启「允许从 GitHub 获取技能」。", "installSkillInvalidGithubUrl": "无效的 GitHub URL:{url}", "installSkillGithubApiFailed": "无法获取仓库信息(HTTP {status})。请检查 URL 是否正确。", @@ -2734,7 +2775,7 @@ "installSkillSuccessLocal": "技能「{name}」安装成功!\n理由:{reason}", "installSkillSizeLimit": "技能内容超过大小上限({size}KB > {max}KB)", "installSkillNoUtility": "安全审查不可用:未配置 utility 模型,请在设置中配置辅助模型", - "installSkillUtilityIncomplete": "安全审查不可用:辅助模型配置不完整(缺少 API key 或 base URL),请在设置中补全", + "installSkillUtilityIncomplete": "安全审查不可用:能力设置不完整(缺少访问密钥或服务地址),请在设置中补全", "installSkillSafetyPassed": "✓ 安全审查已通过", "installSkillSafetyEmpty": "安全审查返回空响应", "installSkillSafetyUnexpected": "安全审查返回未预期的格式: {reply}", @@ -2852,7 +2893,7 @@ "tabPrompt": "System Prompt", "waitingConnection": "等待连接...", "waitingServer": "等待 Server 启动...", - "loading": "加载中...", + "loading": "小安正在读取…", "loadFailed": "加载失败", "refreshBtn": "刷新", "clickRefresh": "点击上方「刷新」加载", @@ -2899,8 +2940,8 @@ "helpAgent": "查看当前 agent", "helpAgentList": "列出所有 agent", "helpAgentSwitch": "切换 agent", - "helpJian": "查看当前目录的笺", - "helpJianSub": "查看子目录的笺", + "helpJian": "查看当前目录的笔记", + "helpJianSub": "查看子目录的笔记", "helpLs": "列出书桌文件", "helpLsSub": "列出子目录文件", "helpCat": "查看文件内容", @@ -2922,11 +2963,11 @@ "sessionList": "会话列表", "untitled": "(无标题)", "sessionUsage": "用法: /session new | /session list", - "agentList": "Agent 列表", + "agentList": "状态列表", "agentSwitched": "已切换到 {name}", - "currentAgent": "当前 Agent:", - "jianTitle": "笺", - "jianEmpty": "此目录没有笺", + "currentAgent": "当前状态:", + "jianTitle": "笔记", + "jianEmpty": "这个目录还没有笔记", "dirEmpty": "(空)", "catUsage": "用法: /cat <文件路径>", "catFailed": "无法读取: {status}", @@ -3166,7 +3207,7 @@ "toolDef": { "updateSettings": { "memoryFacts": "记忆 Facts", - "memoryFactsDesc": "当前 Agent 的可编辑当下记忆 Facts。需要先打开「可编辑记忆」实验。", + "memoryFactsDesc": "当前状态的可编辑当下记忆事实。需要先打开「可编辑记忆」实验。", "memoryFactsDisabled": "可编辑记忆实验尚未打开。" }, "generateImage": { @@ -3176,8 +3217,8 @@ "imageDesc": "图生图参考图。优先使用当前会话对象,例如 { kind: \"session_file\", fileId }。", "referenceImagesDesc": "多张参考图。优先使用当前会话对象,例如 { kind: \"session_file\", fileId };多参考图时用它,单图仍可用 image。", "ratioDesc": "长宽比:1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9", - "resolutionDesc": "统一分辨率档位:1k, 2k, 4k;由 adapter 映射为供应商最接近的尺寸", - "qualityDesc": "画质档位:low, medium, high, auto(由 adapter 映射为供应商参数)", + "resolutionDesc": "统一分辨率档位:1k, 2k, 4k;由 adapter 映射为来源方最接近的尺寸", + "qualityDesc": "画质档位:low, medium, high, auto(由 adapter 映射为来源方参数)", "modelDesc": "模型 ID 或简称(如 5.0、dall-e-3)。普通生成必须省略,使用已配置默认模型;只有用户明确指定模型或查询 options 后确认需要时才填写", "modeDesc": "高级模式 override(text2image / image2image)。默认请省略;有 image/referenceImages 时会自动推断 image2image。不要把 mode 填进 model", "providerDesc": "指定 provider(可选)。普通生成必须省略,使用已配置默认 provider;只有用户明确指定 provider 或查询 options 后确认需要时才填写", diff --git a/desktop/src/mobile-main.tsx b/desktop/src/mobile-main.tsx index d73995053..7cdef4675 100644 --- a/desktop/src/mobile-main.tsx +++ b/desktop/src/mobile-main.tsx @@ -21,7 +21,7 @@ void import('./react/mobile/MobileApp').then(({ MobileApp }) => { ); }).catch((err) => { console.error('[mobile] failed to boot renderer:', err); - root.textContent = '小小安 启动失败'; + root.textContent = '小安启动失败'; }); const MOBILE_UPDATE_AVAILABLE_EVENT = 'hana-mobile-update-available'; diff --git a/desktop/src/mobile-manifest.webmanifest b/desktop/src/mobile-manifest.webmanifest index efe19fc97..a20de8254 100644 --- a/desktop/src/mobile-manifest.webmanifest +++ b/desktop/src/mobile-manifest.webmanifest @@ -1,6 +1,6 @@ { - "name": "HanaAgent", - "short_name": "HanaAgent", + "name": "小安", + "short_name": "小安", "start_url": "./", "scope": "./", "display": "standalone", diff --git a/desktop/src/modules/platform.js b/desktop/src/modules/platform.js index 789f96da6..a1ab80099 100644 --- a/desktop/src/modules/platform.js +++ b/desktop/src/modules/platform.js @@ -1,14 +1,20 @@ /** * platform.js — 平台适配层 * - * Electron 环境:直接转发给 preload 注入的 window.hana(IPC) + * Electron 环境:直接转发给 preload 注入的 window.xiaoan(IPC) * Web 环境:降级到 HTTP API + 浏览器原生 API * - * 使用方式:所有前端代码调 platform.xxx(),不再直接碰 window.hana。 + * 使用方式:所有前端代码调 platform.xxx(),不再直接碰 window.xiaoan/window.hana。 */ (function () { + if (window.xiaoan) { + // Electron — 直接用 preload 注入的小安 IPC bridge + window.platform = window.xiaoan; + return; + } + if (window.hana) { - // Electron — 直接用 preload 注入的 IPC bridge + // Legacy Electron bridge window.platform = window.hana; return; } diff --git a/desktop/src/react/__tests__/components/AppPages.test.tsx b/desktop/src/react/__tests__/components/AppPages.test.tsx index 7a2698993..77ad344d4 100644 --- a/desktop/src/react/__tests__/components/AppPages.test.tsx +++ b/desktop/src/react/__tests__/components/AppPages.test.tsx @@ -77,6 +77,15 @@ describe('AppPages page ownership', () => { channelInfoName: '', jianOpen: true, previewOpen: true, + pluginPages: [ + { + pluginId: 'glance', + title: '概览', + icon: null, + routeUrl: '/api/plugins/glance/page', + hostCapabilities: [], + }, + ], } as never); }); @@ -102,6 +111,16 @@ describe('AppPages page ownership', () => { expect(screen.getByTestId('right-workspace-panel')).toBeInTheDocument(); }); + it('opens the built-in Glance page through the plugin page shell', () => { + useStore.setState({ currentTab: 'plugin:glance' } as never); + + render(); + + expect(screen.getByTestId('plugin-page')).toHaveTextContent('glance'); + expect(document.querySelector('#previewPanel')).not.toBeInTheDocument(); + expect(screen.getByTestId('right-workspace-panel')).toBeInTheDocument(); + }); + it('keeps channel inspector and workspace companion as separate right-side panels', () => { useStore.setState({ currentTab: 'channels', diff --git a/desktop/src/react/__tests__/components/PluginPageView.test.tsx b/desktop/src/react/__tests__/components/PluginPageView.test.tsx index e18e1a76d..25b598313 100644 --- a/desktop/src/react/__tests__/components/PluginPageView.test.tsx +++ b/desktop/src/react/__tests__/components/PluginPageView.test.tsx @@ -2,12 +2,24 @@ * @vitest-environment jsdom */ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { cleanup, render } from '@testing-library/react'; import { PluginPageView } from '../../components/plugin/PluginPageView'; import { usePluginIframe } from '../../hooks/use-plugin-iframe'; +import { usePluginSurfaceUrl } from '../../hooks/use-plugin-surface-url'; -const mockState = { +type MockPluginPage = { + pluginId: string; + routeUrl: string; + hostCapabilities: string[]; +}; + +type MockState = { + pluginPages: MockPluginPage[]; + currentAgentId: string; +}; + +let mockState: MockState = { pluginPages: [{ pluginId: 'plain-plugin', routeUrl: '/api/plugins/plain-plugin/page', @@ -21,9 +33,11 @@ vi.mock('../../stores', () => ({ })); vi.mock('../../hooks/use-plugin-surface-url', () => ({ - usePluginSurfaceUrl: vi.fn(() => ({ + usePluginSurfaceUrl: vi.fn((routeUrl: string | null, agentId: string | null) => ({ status: 'ready', - iframeSrc: 'http://127.0.0.1:3210/api/plugins/plain-plugin/page?ticket=abc', + iframeSrc: routeUrl + ? `http://127.0.0.1:3210${routeUrl}?ticket=abc&agentId=${agentId ?? ''}` + : '', retry: vi.fn(), })), })); @@ -38,16 +52,28 @@ vi.mock('../../hooks/use-plugin-iframe', () => ({ })); describe('PluginPageView', () => { + beforeEach(() => { + mockState = { + pluginPages: [{ + pluginId: 'plain-plugin', + routeUrl: '/api/plugins/plain-plugin/page', + hostCapabilities: [], + }], + currentAgentId: 'agent-1', + }; + }); + afterEach(() => { cleanup(); vi.mocked(usePluginIframe).mockClear(); + vi.mocked(usePluginSurfaceUrl).mockClear(); }); it('lets plain HTML plugin pages become visible even without an SDK ready handshake', () => { render(); expect(usePluginIframe).toHaveBeenCalledWith( - 'http://127.0.0.1:3210/api/plugins/plain-plugin/page?ticket=abc', + 'http://127.0.0.1:3210/api/plugins/plain-plugin/page?ticket=abc&agentId=agent-1', expect.objectContaining({ pluginId: 'plain-plugin', slot: 'page', @@ -55,4 +81,29 @@ describe('PluginPageView', () => { }), ); }); + + it('passes the built-in Glance page through the authenticated Xiaoan plugin surface', () => { + mockState = { + pluginPages: [{ + pluginId: 'glance', + routeUrl: '/api/plugins/glance/page', + hostCapabilities: ['clipboard:read'], + }], + currentAgentId: 'minian', + }; + + render(); + + expect(usePluginSurfaceUrl).toHaveBeenCalledWith('/api/plugins/glance/page', 'minian'); + expect(usePluginIframe).toHaveBeenCalledWith( + 'http://127.0.0.1:3210/api/plugins/glance/page?ticket=abc&agentId=minian', + expect.objectContaining({ + pluginId: 'glance', + agentId: 'minian', + slot: 'page', + readyOnTimeout: true, + capabilityGrants: ['clipboard:read'], + }), + ); + }); }); diff --git a/desktop/src/react/__tests__/components/ProcessFoldBlock.test.tsx b/desktop/src/react/__tests__/components/ProcessFoldBlock.test.tsx index 3f7aab430..23371ec5d 100644 --- a/desktop/src/react/__tests__/components/ProcessFoldBlock.test.tsx +++ b/desktop/src/react/__tests__/components/ProcessFoldBlock.test.tsx @@ -118,7 +118,7 @@ describe('ProcessFoldBlock', () => { expect(screen.queryByText('npm test')).not.toBeInTheDocument(); expect(screen.getByText('正文来了')).toBeInTheDocument(); expect(screen.getAllByText('思考完成')).toHaveLength(1); - expect(screen.getByText(/PULSE/)).toBeInTheDocument(); + expect(screen.getByText(/心绪/)).toBeInTheDocument(); fireEvent.click(summary); diff --git a/desktop/src/react/__tests__/components/RightWorkspacePanel.test.tsx b/desktop/src/react/__tests__/components/RightWorkspacePanel.test.tsx index f2fc3b3f9..d46d10f86 100644 --- a/desktop/src/react/__tests__/components/RightWorkspacePanel.test.tsx +++ b/desktop/src/react/__tests__/components/RightWorkspacePanel.test.tsx @@ -214,7 +214,7 @@ describe('RightWorkspacePanel', () => { 'utf-8', ); const shellBlock = panelCss.match(/\.shell\s*\{[\s\S]*?\}/)?.[0] ?? ''; - const jianInnerBlock = globalCss.match(/\.jian-sidebar-inner\s*\{[\s\S]*?\}/)?.[0] ?? ''; + const jianInnerBlock = globalCss.match(/(?:^|\n)\.jian-sidebar-inner\s*\{[\s\S]*?\}/)?.[0] ?? ''; const floatRightBlock = globalCss.match(/\.float-sidebar\[data-side="right"\]\s*\{[\s\S]*?\}/)?.[0] ?? ''; const rootBlock = globalCss.match(/:root\s*\{[\s\S]*?\}/)?.[0] ?? ''; const universalCardBlock = globalCss.match(/\.universal-card,\s*\.jian-card\s*\{[\s\S]*?\}/)?.[0] ?? ''; diff --git a/desktop/src/react/__tests__/components/SessionListContextMenu.test.tsx b/desktop/src/react/__tests__/components/SessionListContextMenu.test.tsx index fa33d5fcc..1699fa8ce 100644 --- a/desktop/src/react/__tests__/components/SessionListContextMenu.test.tsx +++ b/desktop/src/react/__tests__/components/SessionListContextMenu.test.tsx @@ -78,6 +78,8 @@ function seedSessions() { unreadOutputSessionPaths: [], browserBySession: {}, locale: 'zh', + serverPort: '3210', + serverToken: 'test-token', }); } diff --git a/desktop/src/react/__tests__/components/computer-app-approval-prompt.test.ts b/desktop/src/react/__tests__/components/computer-app-approval-prompt.test.ts index 29788ccf3..073a35ddc 100644 --- a/desktop/src/react/__tests__/components/computer-app-approval-prompt.test.ts +++ b/desktop/src/react/__tests__/components/computer-app-approval-prompt.test.ts @@ -175,7 +175,7 @@ describe('computer app approval prompt', () => { it('renders the current session pending input confirmation above the input box and posts confirmation', async () => { render(React.createElement(InputArea)); - expect(screen.getByText('是否允许 Hana 控制 Mock Notes')).toBeTruthy(); + expect(screen.getByText('是否允许小安控制 Mock Notes')).toBeTruthy(); expect(screen.queryByText('Hana 想控制这个应用来继续当前任务。')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '同意' })); @@ -219,7 +219,7 @@ describe('computer app approval prompt', () => { render(React.createElement(InputArea)); - expect(screen.getByText('是否允许 Hana 控制 Mock Notes')).toBeTruthy(); + expect(screen.getByText('是否允许小安控制 Mock Notes')).toBeTruthy(); }); it('re-enables approval actions when a new pending confirmation replaces the submitted one', async () => { diff --git a/desktop/src/react/__tests__/editor/markdown-link-colors.test.ts b/desktop/src/react/__tests__/editor/markdown-link-colors.test.ts index 67ed42820..efe73f94e 100644 --- a/desktop/src/react/__tests__/editor/markdown-link-colors.test.ts +++ b/desktop/src/react/__tests__/editor/markdown-link-colors.test.ts @@ -11,9 +11,9 @@ describe('markdown link colors', () => { const styles = readFile('desktop/src/styles.css'); const previewStyles = readFile('desktop/src/react/components/Preview.module.css'); - expect(styles).toMatch(/--link:\s*var\(--accent,\s*#537D96\)/); + expect(styles).toMatch(/--link:\s*var\(--accent,\s*#3370ff\)/); expect(styles).toMatch(/--link-hover:\s*var\(--accent-hover,\s*var\(--link\)\)/); - expect(styles).toMatch(/--link-rgb:\s*var\(--accent-rgb,\s*83,\s*125,\s*150\)/); + expect(styles).toMatch(/--link-rgb:\s*var\(--accent-rgb,\s*51,\s*112,\s*255\)/); expect(styles).toMatch(/\.md-content a\s*\{[\s\S]*color:\s*var\(--link\)/); expect(styles).toMatch(/\.md-content a\s*\{[\s\S]*rgba\(var\(--link-rgb\),\s*0\.35\)/); expect(styles).toMatch(/\.md-content a:hover\s*\{[\s\S]*color:\s*var\(--link-hover\)/); diff --git a/desktop/src/react/__tests__/hooks/use-hana-fetch.test.ts b/desktop/src/react/__tests__/hooks/use-hana-fetch.test.ts index c34cab1aa..608b1c619 100644 --- a/desktop/src/react/__tests__/hooks/use-hana-fetch.test.ts +++ b/desktop/src/react/__tests__/hooks/use-hana-fetch.test.ts @@ -10,7 +10,7 @@ vi.mock('../../stores', () => ({ kind: 'local', serverId: 'local', studioId: 'local', - label: 'Local Hana', + label: 'Local Xiaoan', baseUrl: 'http://127.0.0.1:3210', wsUrl: 'ws://127.0.0.1:3210', token: 'test-token-123', diff --git a/desktop/src/react/__tests__/services/resource-url.test.ts b/desktop/src/react/__tests__/services/resource-url.test.ts index 441cc7586..777e4d841 100644 --- a/desktop/src/react/__tests__/services/resource-url.test.ts +++ b/desktop/src/react/__tests__/services/resource-url.test.ts @@ -9,7 +9,7 @@ const localConnection: ServerConnection = { serverId: 'server_local', userId: 'user_local', studioId: 'studio_local', - label: 'Local Hana', + label: 'Local Xiaoan', baseUrl: 'http://127.0.0.1:14500', wsUrl: 'ws://127.0.0.1:14500', token: 'local token', diff --git a/desktop/src/react/__tests__/services/server-connection.test.ts b/desktop/src/react/__tests__/services/server-connection.test.ts index d5188c508..df55b4f20 100644 --- a/desktop/src/react/__tests__/services/server-connection.test.ts +++ b/desktop/src/react/__tests__/services/server-connection.test.ts @@ -30,7 +30,7 @@ describe('server connection helpers', () => { kind: 'local', serverId: 'local', studioId: 'local', - label: 'Local Hana', + label: 'Local Xiaoan', baseUrl: 'http://127.0.0.1:3210', wsUrl: 'ws://127.0.0.1:3210', token: 'test-token-123', diff --git a/desktop/src/react/__tests__/settings/AccessTab.test.tsx b/desktop/src/react/__tests__/settings/AccessTab.test.tsx index 479a49505..469d4a6b7 100644 --- a/desktop/src/react/__tests__/settings/AccessTab.test.tsx +++ b/desktop/src/react/__tests__/settings/AccessTab.test.tsx @@ -103,7 +103,7 @@ const localConnection = { kind: 'local', serverId: 'local', studioId: 'local', - label: 'Local Hana', + label: 'Local Xiaoan', baseUrl: 'http://127.0.0.1:14500', wsUrl: 'ws://127.0.0.1:14500', token: 'local', diff --git a/desktop/src/react/__tests__/settings/AgentTab.test.tsx b/desktop/src/react/__tests__/settings/AgentTab.test.tsx index 345075806..d0dad94ca 100644 --- a/desktop/src/react/__tests__/settings/AgentTab.test.tsx +++ b/desktop/src/react/__tests__/settings/AgentTab.test.tsx @@ -112,6 +112,7 @@ describe('AgentTab settings agent selection', () => { showInFinderMock.mockReset(); (window as unknown as { platform: unknown }).platform = { showInFinder: showInFinderMock }; useSettingsStore.setState({ + developerModeEnabled: true, agents: [ { id: 'hana', name: 'Hana', yuan: 'hanako', isPrimary: true }, { id: 'deepseek', name: 'DeepSeek', yuan: 'deepseek', isPrimary: false }, @@ -135,6 +136,18 @@ describe('AgentTab settings agent selection', () => { delete (window as unknown as { platform?: unknown }).platform; }); + it('hides developer sections when developer mode is off', async () => { + useSettingsStore.setState({ developerModeEnabled: false }); + const { AgentTab } = await import('../../settings/tabs/AgentTab'); + render(); + + expect(screen.getByPlaceholderText('settings.agent.agentNameHint')).toBeInTheDocument(); + expect(screen.queryByTestId('yuan-selector')).toBeNull(); + expect(screen.queryByTestId('memory-section')).toBeNull(); + expect(screen.queryByTestId('model-select')).toBeNull(); + expect(screen.queryByTestId('agent-tools')).toBeNull(); + }); + it('rerenders when browsing a different settings agent', async () => { const { AgentTab } = await import('../../settings/tabs/AgentTab'); render(); diff --git a/desktop/src/react/__tests__/settings/ProvidersTab.builtin-relay.test.tsx b/desktop/src/react/__tests__/settings/ProvidersTab.builtin-relay.test.tsx new file mode 100644 index 000000000..401e63930 --- /dev/null +++ b/desktop/src/react/__tests__/settings/ProvidersTab.builtin-relay.test.tsx @@ -0,0 +1,103 @@ +/** + * @vitest-environment jsdom + */ + +import React from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { useSettingsStore } from '../../settings/store'; +import { XIAOAN_BUILTIN_RELAY_PROVIDER_ID } from '../../../../../shared/xiaoan-builtin-relay.ts'; + +const mocks = vi.hoisted(() => ({ + hanaFetch: vi.fn(), + loadSettingsConfig: vi.fn(async () => {}), +})); + +vi.mock('../../settings/api', () => ({ + hanaFetch: (...args: unknown[]) => mocks.hanaFetch(...args), +})); + +vi.mock('../../settings/actions', () => ({ + loadSettingsConfig: () => mocks.loadSettingsConfig(), +})); + +vi.mock('../../settings/helpers', () => ({ + t: (key: string) => key, + PROVIDER_PRESETS: [], + lookupModelMeta: () => null, + formatContext: () => '', + autoSaveGlobalModels: vi.fn(), +})); + +vi.mock('../../settings/tabs/providers/OtherModelsSection', () => ({ + OtherModelsSection: ({ hideSearch }: { hideSearch?: boolean }) => ( +
+ ), +})); + +vi.mock('../../settings/tabs/providers/ProviderModelList', () => ({ + ProviderModelList: () =>
, +})); + +vi.mock('../../settings/tabs/providers/OAuthCredentials', () => ({ + OAuthCredentials: () =>
, +})); + +vi.mock('../../settings/tabs/providers/ApiKeyCredentials', () => ({ + ApiKeyCredentials: ({ credentialsMode }: { credentialsMode?: string }) => ( +
+ ), +})); + +vi.mock('@/ui', () => ({ + ProviderIcon: ({ provider }: { provider?: string }) =>
, +})); + +import { ProvidersTab } from '../../settings/tabs/ProvidersTab'; + +function jsonResponse(body: unknown): Response { + return { json: async () => body } as Response; +} + +describe('ProvidersTab builtin relay mode', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.hanaFetch.mockResolvedValue(jsonResponse({ + providers: { + [XIAOAN_BUILTIN_RELAY_PROVIDER_ID]: { + display_name: '安芯跃动中转站', + base_url: 'https://relay.example.com/v1', + api: 'openai-completions', + is_configured: false, + }, + }, + })); + useSettingsStore.setState({ + developerModeEnabled: false, + providersSummary: {}, + selectedProviderId: null, + settingsConfig: { providers: {} }, + }); + }); + + afterEach(() => { + cleanup(); + }); + + it('shows only the builtin relay provider UI when developer mode is off', async () => { + render(); + + expect(await screen.findByRole('heading', { name: 'settings.providers.builtinRelay.title' })).toBeInTheDocument(); + expect(screen.getByText('settings.providers.builtinRelay.integrationNote')).toBeInTheDocument(); + expect(screen.getByText('settings.providers.builtinRelay.knownIssuesTitle')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'settings.providers.builtinRelay.consoleAction' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'settings.providers.builtinRelay.tokenAction' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'settings.providers.builtinRelay.rechargeAction' })).toBeInTheDocument(); + expect(screen.getByTestId(`provider-icon-${XIAOAN_BUILTIN_RELAY_PROVIDER_ID}`)).toBeInTheDocument(); + expect(screen.getByTestId('api-key-credentials')).toHaveAttribute('data-mode', 'builtin-relay'); + expect(screen.getByTestId('other-models-section')).toHaveAttribute('data-hide-search', 'true'); + expect(screen.queryByText('settings.providers.addCustom')).not.toBeInTheDocument(); + expect(useSettingsStore.getState().selectedProviderId).toBe(XIAOAN_BUILTIN_RELAY_PROVIDER_ID); + }); +}); diff --git a/desktop/src/react/__tests__/settings/ProvidersTab.test.tsx b/desktop/src/react/__tests__/settings/ProvidersTab.test.tsx index 4469c4dc2..cdb454755 100644 --- a/desktop/src/react/__tests__/settings/ProvidersTab.test.tsx +++ b/desktop/src/react/__tests__/settings/ProvidersTab.test.tsx @@ -86,6 +86,7 @@ describe('ProvidersTab provider-scoped form state', () => { beforeEach(() => { vi.clearAllMocks(); + useSettingsStore.setState({ developerModeEnabled: true }); mocks.hanaFetch.mockImplementation((path: string) => { if (path === '/api/providers/summary') { return Promise.resolve(jsonResponse({ providers: providersSummary })); diff --git a/desktop/src/react/__tests__/settings/SettingsContent.test.tsx b/desktop/src/react/__tests__/settings/SettingsContent.test.tsx index e9753073b..5c0a495ce 100644 --- a/desktop/src/react/__tests__/settings/SettingsContent.test.tsx +++ b/desktop/src/react/__tests__/settings/SettingsContent.test.tsx @@ -83,6 +83,7 @@ function resetState() { Object.keys(mockState).forEach(key => delete mockState[key]); Object.assign(mockState, { activeTab: 'agent', + developerModeEnabled: false, ready: true, set: vi.fn((patch: Record) => Object.assign(mockState, patch)), }); @@ -137,6 +138,7 @@ describe('SettingsContent title placement', () => { it('notifies the modal shell when the active settings tab changes', async () => { const onActiveTabChange = vi.fn(); + mockState.developerModeEnabled = true; const { SettingsContent } = await import('../../settings/SettingsContent'); render( {}} onActiveTabChange={onActiveTabChange} />); @@ -186,6 +188,7 @@ describe('SettingsContent title placement', () => { it('does not render a Computer Use tab and redirects stale computer tabs to experiments', async () => { mockState.activeTab = 'computer'; + mockState.developerModeEnabled = true; const { SettingsContent } = await import('../../settings/SettingsContent'); render( {}} />); diff --git a/desktop/src/react/__tests__/shared/appearance-preferences.test.ts b/desktop/src/react/__tests__/shared/appearance-preferences.test.ts index 08aab5ada..46c5026be 100644 --- a/desktop/src/react/__tests__/shared/appearance-preferences.test.ts +++ b/desktop/src/react/__tests__/shared/appearance-preferences.test.ts @@ -18,6 +18,9 @@ function createStorage() { setItem: (key: string, value: string) => { values.set(key, value); }, + removeItem: (key: string) => { + values.delete(key); + }, }; } @@ -74,11 +77,14 @@ describe('paper texture preferences', () => { it('restores texture automatically when a stored preference returns to a supported theme', () => { storage.setItem(PAPER_TEXTURE_STORAGE_KEY, '1'); + const paperFriendlyTheme = registry.getThemeIds().find( + (id) => !registry.isPaperTextureBlockedTheme(id), + )!; expect(loadPaperTexturePreference(storage, document.body, registry.PAPER_TEXTURE_BLOCKED_THEME_IDS[1])).toBe(true); expect(document.body.classList.contains(PAPER_TEXTURE_CLASS)).toBe(false); - expect(loadPaperTexturePreference(storage, document.body, registry.AUTO_LIGHT_DEFAULT)).toBe(true); + expect(loadPaperTexturePreference(storage, document.body, paperFriendlyTheme)).toBe(true); expect(document.body.classList.contains(PAPER_TEXTURE_CLASS)).toBe(true); }); }); diff --git a/desktop/src/react/__tests__/utils/message-parser.test.ts b/desktop/src/react/__tests__/utils/message-parser.test.ts index 2c82fc551..6680216e8 100644 --- a/desktop/src/react/__tests__/utils/message-parser.test.ts +++ b/desktop/src/react/__tests__/utils/message-parser.test.ts @@ -245,8 +245,12 @@ describe('moodLabel', () => { expect(moodLabel('hanako')).toBe('状态'); }); - it('butter 返回 PULSE', () => { - expect(moodLabel('butter')).toContain('PULSE'); + it('butter 返回心绪', () => { + expect(moodLabel('butter')).toContain('心绪'); + }); + + it('ming 返回沉思', () => { + expect(moodLabel('ming')).toContain('沉思'); }); it('未知 yuan 降级为默认状态', () => { diff --git a/desktop/src/react/__tests__/utils/screenshot.test.ts b/desktop/src/react/__tests__/utils/screenshot.test.ts index 5b968ee3b..ac986e8d9 100644 --- a/desktop/src/react/__tests__/utils/screenshot.test.ts +++ b/desktop/src/react/__tests__/utils/screenshot.test.ts @@ -322,7 +322,7 @@ describe('screenshot utils', () => { const pngBlob = new Blob([new Uint8Array([137, 80, 78, 71])], { type: 'image/png' }); const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); - if (url.includes('assets/Hanako.png')) { + if (url.includes('assets/Xiaoan.png')) { return new Response(pngBlob, { status: 200, headers: { 'Content-Type': 'image/png' } }); } return new Response('', { status: 404 }); @@ -339,8 +339,8 @@ describe('screenshot utils', () => { const payload = (window as any).hana.screenshotRender.mock.calls[0][0]; expect(payload.messages[0].avatarDataUrl).toMatch(/^data:image\/svg\+xml/); expect(decodeURIComponent(payload.messages[0].avatarDataUrl)).toContain('唐'); - expect(payload.messages[1].avatarDataUrl).toMatch(/^data:image\/png;base64,/); - expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('assets/Hanako.png')); + expect(payload.messages[1].avatarDataUrl).toMatch(/^data:image\/svg\+xml/); + expect(fetchMock).not.toHaveBeenCalled(); }); it('用户消息截图 payload 保留非图片附件的语义块', async () => { diff --git a/desktop/src/react/app-init.ts b/desktop/src/react/app-init.ts index 43cc7f6c1..9354f2832 100644 --- a/desktop/src/react/app-init.ts +++ b/desktop/src/react/app-init.ts @@ -25,6 +25,7 @@ import { configureAppEventActions, handleAppEvent, readConfigCwdHistory, readCon import { configureWsMessageHandler } from './services/ws-message-handler'; import { applyChatLayout } from './chat/layout'; import { applyEditorTypography } from './editor/typography'; +import { installMotionVisibilityFailsafe } from './motion/motion-failsafe'; import { LOCAL_CONNECTION_ID, createLocalServerConnection, @@ -225,6 +226,7 @@ export async function initApp(): Promise { initErrorBusBridge(); markRendererLaunch('app-ready'); platform.appReady(); + installMotionVisibilityFailsafe(); // 以下初始化在 UI 显示后继续 await loadModels(); diff --git a/desktop/src/react/components/SkillViewerOverlay.tsx b/desktop/src/react/components/SkillViewerOverlay.tsx index 99a3389e9..456d32c21 100644 --- a/desktop/src/react/components/SkillViewerOverlay.tsx +++ b/desktop/src/react/components/SkillViewerOverlay.tsx @@ -143,7 +143,7 @@ export function SkillViewerOverlay() {
- {data.name || 'Skill'} + {data.name || '能力'} / {fileName}
diff --git a/desktop/src/react/components/Welcome.module.css b/desktop/src/react/components/Welcome.module.css index 1a993af91..188bc292f 100644 --- a/desktop/src/react/components/Welcome.module.css +++ b/desktop/src/react/components/Welcome.module.css @@ -8,8 +8,8 @@ align-items: center; justify-content: center; min-height: 0; - gap: 0.85rem; - padding: 0 1.25rem 0.75rem; + gap: 1rem; + padding: 0 1rem 0.5rem; text-align: center; max-width: 36rem; margin-inline: auto; @@ -86,63 +86,63 @@ border: 1px solid var(--console-hairline, var(--border)); } -:global(html[data-theme="xiaoan-console"]) .welcomeTitle, -:global(html[data-theme="xiaoan-console-dark"]) .welcomeTitle { - font-family: var(--font-ui); +:global(html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"])) .welcomeTitle, +:global(html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"])) .welcomeTitle { + font-family: var(--font-serif); font-weight: 600; - letter-spacing: -0.02em; - font-size: clamp(1.5rem, 3.2vw, 1.875rem); + letter-spacing: -0.01em; + font-size: clamp(1.35rem, 3vw, 1.75rem); color: var(--text); line-height: 1.25; } -:global(html[data-theme="xiaoan-console"]) .welcomeTitle::after, -:global(html[data-theme="xiaoan-console-dark"]) .welcomeTitle::after { +:global(html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"])) .welcomeTitle::after, +:global(html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"])) .welcomeTitle::after { display: none; } -:global(html[data-theme="xiaoan-console"]) .welcomeTagline, -:global(html[data-theme="xiaoan-console-dark"]) .welcomeTagline { +:global(html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"])) .welcomeTagline, +:global(html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"])) .welcomeTagline { color: var(--text-muted); - font-family: var(--font-ui); + font-family: var(--font-serif); font-weight: 400; letter-spacing: 0; font-size: 0.8125rem; } -:global(html[data-theme="xiaoan-console"]) .welcomeEpigraph::before, -:global(html[data-theme="xiaoan-console-dark"]) .welcomeEpigraph::before { +:global(html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"])) .welcomeEpigraph::before, +:global(html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"])) .welcomeEpigraph::before { display: none; } -:global(html[data-theme="xiaoan-console"]) .welcomeText, -:global(html[data-theme="xiaoan-console-dark"]) .welcomeText { - font-family: var(--font-ui); +:global(html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"])) .welcomeText, +:global(html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"])) .welcomeText { + font-family: var(--font-serif); font-weight: 400; - letter-spacing: 0.01em; - font-size: 0.9375rem; + letter-spacing: 0.005em; + font-size: 0.875rem; color: var(--text-muted); - line-height: 1.6; + line-height: 1.55; margin: 0; } -:global(html[data-theme="xiaoan-console"]) .welcomeHint, -:global(html[data-theme="xiaoan-console-dark"]) .welcomeHint { - font-family: var(--font-ui); +:global(html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"])) .welcomeHint, +:global(html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"])) .welcomeHint { + font-family: var(--font-serif); letter-spacing: 0; - font-size: 0.75rem; - border: 1px solid var(--border); + font-size: 0.72rem; + border: var(--border-width, 1px) solid var(--border); background: var(--bg-card); box-shadow: none; - border-radius: 8px; - padding: 0.35rem 0.7rem; + border-radius: 0; + padding: 0.28rem 0.6rem; } -:global(html[data-theme="xiaoan-console"]) .welcomeAvatar, -:global(html[data-theme="xiaoan-console-dark"]) .welcomeAvatar { - border-radius: 50%; - border: none; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); +:global(html:is([data-theme="xiaoan-console"], [data-theme="xiaoan-claude"], [data-theme="xiaoan-claude-default"], [data-theme="claude-design"])) .welcomeAvatar, +:global(html:is([data-theme="xiaoan-console-dark"], [data-theme="xiaoan-claude-dark"])) .welcomeAvatar { + border-radius: 0; + border: var(--border-width, 1px) solid var(--border); + box-shadow: none; width: 72px; height: 72px; object-fit: cover; diff --git a/desktop/src/react/components/WelcomeScreen.tsx b/desktop/src/react/components/WelcomeScreen.tsx index 82632f52c..473fd76bf 100644 --- a/desktop/src/react/components/WelcomeScreen.tsx +++ b/desktop/src/react/components/WelcomeScreen.tsx @@ -30,8 +30,6 @@ import { buildWorkspacePickerItems, normalizeWorkspacePath } from '../../../../s /* eslint-disable @typescript-eslint/no-explicit-any -- store setState 回调 (s: any) */ -export function refreshAvatarTs() { refreshAgentAvatarVersion(); } - // ── 主组件 ── export function WelcomeScreen() { diff --git a/desktop/src/react/components/app/ChatSidebar.tsx b/desktop/src/react/components/app/ChatSidebar.tsx index f702f9a6d..595ace82a 100644 --- a/desktop/src/react/components/app/ChatSidebar.tsx +++ b/desktop/src/react/components/app/ChatSidebar.tsx @@ -27,11 +27,6 @@ function AutomationBadge() { return {count > 0 ? String(count) : ''}; } -function BridgeDot() { - const connected = useStore(s => s.bridgeDotConnected); - return ; -} - export function ChatSidebarContent({ showSettingsButton = true, showActivityBars = true, @@ -74,14 +69,6 @@ export function ChatSidebarContent({ {showActivityBars && ( <> - + ); +} + export function SettingsNav({ onTabChange }: SettingsNavProps) { - const { activeTab, pluginSettingsTabs } = useSettingsStore( - useShallow(s => ({ activeTab: s.activeTab, pluginSettingsTabs: s.pluginSettingsTabs })) + const { activeTab, pluginSettingsTabs, developerModeEnabled } = useSettingsStore( + useShallow(s => ({ + activeTab: s.activeTab, + pluginSettingsTabs: s.pluginSettingsTabs, + developerModeEnabled: s.developerModeEnabled, + })) ); const set = useSettingsStore(s => s.set); const [query, setQuery] = useState(''); const navItems = buildNavItems(pluginSettingsTabs || []); + const visibleNavItems = useMemo( + () => navItems.filter(item => isSettingsNavItemVisible(item, developerModeEnabled)), + [navItems, developerModeEnabled], + ); + const coreNavItems = useMemo( + () => visibleNavItems.filter(item => item.tier === 'core'), + [visibleNavItems], + ); + const developerNavItems = useMemo( + () => visibleNavItems.filter(item => item.tier === 'developer'), + [visibleNavItems], + ); const activeNavTab = activeTab === 'plugin-marketplace' ? 'plugins' : activeTab; const trimmedQuery = query.trim(); - const searchEntries = useMemo(() => buildSettingsSearchEntries(navItems), [navItems]); + const searchEntries = useMemo( + () => buildSettingsSearchEntries(visibleNavItems, developerModeEnabled), + [visibleNavItems, developerModeEnabled], + ); const searchResults = useMemo( () => searchSettings(trimmedQuery, searchEntries, t), [searchEntries, trimmedQuery], @@ -137,17 +166,24 @@ export function SettingsNav({ onTabChange }: SettingsNavProps) {
{t('settings.settingsSearch.noResults')}
)}
- ) : navItems.map(item => ( - - ))} + ) : ( + <> +
+ {coreNavItems.map(item => ( + + ))} +
+ + {developerModeEnabled && developerNavItems.length > 0 && ( +
+
{t('settings.developer.navGroup')}
+ {developerNavItems.map(item => ( + + ))} +
+ )} + + )} ); } diff --git a/desktop/src/react/settings/__tests__/SettingsContent.test.tsx b/desktop/src/react/settings/__tests__/SettingsContent.test.tsx index 2c4c3a8fb..0a2d21254 100644 --- a/desktop/src/react/settings/__tests__/SettingsContent.test.tsx +++ b/desktop/src/react/settings/__tests__/SettingsContent.test.tsx @@ -20,6 +20,9 @@ vi.mock('../api', () => ({ if (url === '/api/config') { return new Response(JSON.stringify({ locale: 'zh-CN' })); } + if (url === '/api/preferences/settings-ui') { + return new Response(JSON.stringify({ settingsUi: { developerModeEnabled: true } })); + } return new Response(JSON.stringify({ experiments: [] })); }), })); @@ -48,6 +51,7 @@ describe('SettingsContent tab heading', () => { activeTab: 'experiments', platformName: 'darwin', pluginSettingsTabs: [], + developerModeEnabled: true, ready: true, } as never); }); @@ -73,6 +77,7 @@ describe('SettingsContent tab heading', () => { activeTab: 'browser', platformName: 'darwin', pluginSettingsTabs: [], + developerModeEnabled: true, ready: true, settingsSnapshot: { status: 'ready', diff --git a/desktop/src/react/settings/__tests__/SettingsNav.search.test.tsx b/desktop/src/react/settings/__tests__/SettingsNav.search.test.tsx index fc588fecd..d17250c5d 100644 --- a/desktop/src/react/settings/__tests__/SettingsNav.search.test.tsx +++ b/desktop/src/react/settings/__tests__/SettingsNav.search.test.tsx @@ -20,6 +20,7 @@ const translations: Record = { 'settings.tabs.skills': '技能', 'settings.tabs.bridge': '桥接', 'settings.tabs.providers': '模型', + 'settings.providers.builtinRelay.title': '安芯跃动中转站', 'settings.tabs.media': '媒体', 'settings.tabs.sharing': '分享', 'settings.tabs.access': '访问', @@ -30,6 +31,7 @@ const translations: Record = { 'settings.api.apiKey': 'API Key', 'settings.api.searchProvider': '搜索服务', 'settings.appearance.theme': '主题', + 'settings.developer.navGroup': '高级', }; describe('SettingsNav search', () => { @@ -46,6 +48,7 @@ describe('SettingsNav search', () => { }; useSettingsStore.setState({ activeTab: 'agent', + developerModeEnabled: false, pluginSettingsTabs: [ { pluginId: 'native', @@ -67,16 +70,16 @@ describe('SettingsNav search', () => { render(React.createElement(SettingsNav, { onTabChange })); const input = screen.getByPlaceholderText('搜索设置'); - fireEvent.change(input, { target: { value: 'api key' } }); + fireEvent.change(input, { target: { value: 'theme' } }); expect(screen.getByText('搜索结果')).toBeTruthy(); - expect(screen.getByRole('button', { name: /API Key/ })).toBeTruthy(); + expect(screen.getByRole('button', { name: /主题/ })).toBeTruthy(); expect(screen.queryByRole('button', { name: '助手' })).toBeNull(); - fireEvent.click(screen.getByRole('button', { name: /API Key/ })); + fireEvent.click(screen.getByRole('button', { name: /主题/ })); - expect(useSettingsStore.getState().activeTab).toBe('providers'); - expect(onTabChange).toHaveBeenCalledWith('providers'); + expect(useSettingsStore.getState().activeTab).toBe('interface'); + expect(onTabChange).toHaveBeenCalledWith('interface'); }); it('clears back to the normal tab list', () => { @@ -90,4 +93,34 @@ describe('SettingsNav search', () => { expect((screen.getByPlaceholderText('搜索设置') as HTMLInputElement).value).toBe(''); expect(screen.getByRole('button', { name: '助手' })).toBeTruthy(); }); + + it('shows providers tab by default', () => { + render(React.createElement(SettingsNav)); + + expect(screen.getByRole('button', { name: '模型' })).toBeTruthy(); + expect(screen.queryByRole('button', { name: '技能' })).toBeNull(); + }); + + it('hides developer tabs by default', () => { + render(React.createElement(SettingsNav)); + + expect(screen.getByRole('button', { name: '助手' })).toBeTruthy(); + expect(screen.getByRole('button', { name: '模型' })).toBeTruthy(); + expect(screen.queryByRole('button', { name: '技能' })).toBeNull(); + }); + + it('shows developer tabs when developer mode is enabled', () => { + useSettingsStore.setState({ developerModeEnabled: true } as never); + render(React.createElement(SettingsNav)); + + expect(screen.getByRole('button', { name: '技能' })).toBeTruthy(); + expect(screen.getByText('高级')).toBeTruthy(); + }); + + it('does not surface developer-only search results when mode is off', () => { + render(React.createElement(SettingsNav)); + + fireEvent.change(screen.getByPlaceholderText('搜索设置'), { target: { value: 'tavily' } }); + expect(screen.getByText('没有找到相关设置')).toBeTruthy(); + }); }); diff --git a/desktop/src/react/settings/components/DeveloperOnly.tsx b/desktop/src/react/settings/components/DeveloperOnly.tsx new file mode 100644 index 000000000..641376d97 --- /dev/null +++ b/desktop/src/react/settings/components/DeveloperOnly.tsx @@ -0,0 +1,23 @@ +/** + * 开发者模式门控:子树仅在 developerModeEnabled 时挂载。 + * 不阻止 API 写入;与 settings-search-index.developerOnly 需手动对齐。 + * + * @see shared/xiaoan-settings-product-notes.ts · XIAOAN_SETTINGS_SECTION_GATING + */ +import React from 'react'; +import { useSettingsStore } from '../store'; + +interface DeveloperOnlyProps { + children: React.ReactNode; +} + +/** 仅在开发者模式下渲染子内容 */ +export function DeveloperOnly({ children }: DeveloperOnlyProps) { + const developerModeEnabled = useSettingsStore(s => s.developerModeEnabled); + if (!developerModeEnabled) return null; + return <>{children}; +} + +export function useDeveloperModeEnabled(): boolean { + return useSettingsStore(s => s.developerModeEnabled); +} diff --git a/desktop/src/react/settings/components/ProductKnownIssues.tsx b/desktop/src/react/settings/components/ProductKnownIssues.tsx new file mode 100644 index 000000000..a54133541 --- /dev/null +++ b/desktop/src/react/settings/components/ProductKnownIssues.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { t } from '../helpers'; +import { SettingsSection } from './SettingsSection'; +import styles from '../Settings.module.css'; + +interface ProductKnownIssuesProps { + titleKey: string; + issueKeys: readonly string[]; +} + +/** 渲染产品层「已知限制」列表(键与 shared/xiaoan-settings-product-notes.ts 同步) */ +export function ProductKnownIssues({ titleKey, issueKeys }: ProductKnownIssuesProps) { + if (issueKeys.length === 0) return null; + return ( + +
    + {issueKeys.map((key) => ( +
  • {t(key)}
  • + ))} +
+
+ ); +} diff --git a/desktop/src/react/settings/helpers.ts b/desktop/src/react/settings/helpers.ts index 99cf03864..ddef83d11 100644 --- a/desktop/src/react/settings/helpers.ts +++ b/desktop/src/react/settings/helpers.ts @@ -173,11 +173,10 @@ export const OUTPUT_PRESETS = [ { label: '64K', value: 65536 }, ]; -const _ids = registry.getThemeIds(); -const _legacyStart = _ids.filter((id) => !['xiaoan-console', 'xiaoan-console-dark'].includes(id)); +const _xiaoanIds = registry.getThemeIds().filter((id) => id.startsWith('xiaoan-')); +const _legacyStart = registry.getThemeIds().filter((id) => !_xiaoanIds.includes(id)); export const VALID_THEMES = [ - 'xiaoan-console', - 'xiaoan-console-dark', + ..._xiaoanIds, registry.AUTO_OPTION.id, ..._legacyStart, ]; diff --git a/desktop/src/react/settings/overlays/CharacterCardPreviewOverlay.tsx b/desktop/src/react/settings/overlays/CharacterCardPreviewOverlay.tsx index 872b71fcf..bdc4f1145 100644 --- a/desktop/src/react/settings/overlays/CharacterCardPreviewOverlay.tsx +++ b/desktop/src/react/settings/overlays/CharacterCardPreviewOverlay.tsx @@ -141,11 +141,11 @@ export function CharacterCardPreviewOverlay({
-

Skills

+

能力

{plan.skills.bundles.length > 0 ? plan.skills.bundles.map((bundle) => (
{bundle.name} - {bundle.skillCount} skills + {bundle.skillCount} 个技能
    {bundle.skills.map(skill =>
  • {skill.name}
  • )}
diff --git a/desktop/src/react/settings/overlays/CompiledMemoryViewer.tsx b/desktop/src/react/settings/overlays/CompiledMemoryViewer.tsx index e1262b8ed..7b26b51ba 100644 --- a/desktop/src/react/settings/overlays/CompiledMemoryViewer.tsx +++ b/desktop/src/react/settings/overlays/CompiledMemoryViewer.tsx @@ -116,7 +116,7 @@ export function CompiledMemoryViewer() {
{loading ? ( -
Loading...
+
{t('settings.memory.compiledLoading')}
) : editableFactsEnabled ? (
- {/* 关于 Ta(保持原样,不改) */} +
-

{t('settings.about.title')}

+

{t('settings.agent.yuanTitle')}

{t('settings.agent.yuanHint')}
-
- - +
+
+ +
+
+ `; + + document.getElementById("glance-reload")?.addEventListener("click", () => loadConfig()); + document.getElementById("glance-save")?.addEventListener("click", () => saveConfig()); + document.getElementById("glance-open")?.addEventListener("click", () => { + window.open(state.serverUrl, "_blank", "noopener,noreferrer"); + }); + document.getElementById("glance-save-url")?.addEventListener("click", () => saveServerUrl()); + document.getElementById("glance-editor")?.addEventListener("input", (event) => { + state.yaml = event.currentTarget.value; + state.dirty = true; + setStatus("未保存的更改"); + }); + setStatus(state.status, state.error); +} + +function escapeHtml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +async function loadConfig() { + try { + const data = await api("/config"); + state.yaml = data.yaml || ""; + state.serverUrl = data.serverUrl || state.serverUrl; + state.widgets = Array.isArray(data.widgets) ? data.widgets : []; + state.dirty = false; + setStatus("已加载"); + render(); + } catch (err) { + setStatus(err.message || String(err), true); + } +} + +async function saveConfig() { + try { + const data = await api("/config", { + method: "PUT", + body: JSON.stringify({ yaml: state.yaml }), + }); + state.yaml = data.yaml || state.yaml; + state.widgets = Array.isArray(data.widgets) ? data.widgets : state.widgets; + state.dirty = false; + setStatus("配置已保存"); + render(); + } catch (err) { + setStatus(err.message || String(err), true); + } +} + +async function saveServerUrl() { + const input = document.getElementById("glance-server-url"); + const serverUrl = input instanceof HTMLInputElement ? input.value.trim() : state.serverUrl; + try { + const data = await api("/settings/server-url", { + method: "PUT", + body: JSON.stringify({ serverUrl }), + }); + state.serverUrl = data.serverUrl || serverUrl; + setStatus("概览地址已保存"); + render(); + } catch (err) { + setStatus(err.message || String(err), true); + } +} + +render(); +loadConfig(); diff --git a/plugins/glance/index.ts b/plugins/glance/index.ts new file mode 100644 index 000000000..3343c758b --- /dev/null +++ b/plugins/glance/index.ts @@ -0,0 +1,7 @@ +export default class GlancePlugin { + declare ctx: any; + + async onload() { + this.ctx.log.info("glance plugin loaded"); + } +} diff --git a/plugins/glance/lib/default-config.ts b/plugins/glance/lib/default-config.ts new file mode 100644 index 000000000..dd3c945ee --- /dev/null +++ b/plugins/glance/lib/default-config.ts @@ -0,0 +1,19 @@ +export const DEFAULT_GLANCE_YAML = `server: + host: 127.0.0.1 + port: 8080 + +pages: + - name: Home + columns: + - size: full + widgets: + - type: monitor + title: Services + cache: 1m + sites: + - title: Xiaoan + url: http://127.0.0.1:3000 + icon: mdi:home-assistant + - type: clock + format: 24h +`; diff --git a/plugins/glance/lib/glance-config.ts b/plugins/glance/lib/glance-config.ts new file mode 100644 index 000000000..5cca2ea1b --- /dev/null +++ b/plugins/glance/lib/glance-config.ts @@ -0,0 +1,145 @@ +import fs from "fs"; +import path from "path"; +import YAML from "js-yaml"; +import { DEFAULT_GLANCE_YAML } from "./default-config.ts"; + +export interface GlanceWidgetRef { + pageIndex: number; + columnIndex: number; + widgetIndex: number; + type: string; + title: string; +} + +export interface GlanceMonitorSite { + title: string; + url: string; + icon?: string; +} + +export function glanceConfigPath(dataDir: string): string { + return path.join(dataDir, "glance.yml"); +} + +export function readGlanceConfig(dataDir: string): { yaml: string; parsed: Record } { + const filePath = glanceConfigPath(dataDir); + if (!fs.existsSync(filePath)) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, DEFAULT_GLANCE_YAML, "utf8"); + return { yaml: DEFAULT_GLANCE_YAML, parsed: YAML.load(DEFAULT_GLANCE_YAML) as Record }; + } + const yaml = fs.readFileSync(filePath, "utf8"); + const parsed = YAML.load(yaml); + return { + yaml, + parsed: parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? (parsed as Record) + : {}, + }; +} + +export function writeGlanceConfig(dataDir: string, yaml: string): { yaml: string; parsed: Record } { + const parsed = YAML.load(yaml); + if (!parsed || typeof parsed !== "object") { + throw new Error("glance.yml must be a YAML object"); + } + const filePath = glanceConfigPath(dataDir); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, yaml, "utf8"); + return readGlanceConfig(dataDir); +} + +export function listGlanceWidgets(parsed: Record): GlanceWidgetRef[] { + const pages = Array.isArray(parsed.pages) ? parsed.pages : []; + const widgets: GlanceWidgetRef[] = []; + for (const [pageIndex, page] of pages.entries()) { + const columns = page && typeof page === "object" && Array.isArray((page as any).columns) + ? (page as any).columns + : []; + for (const [columnIndex, column] of columns.entries()) { + const columnWidgets = column && typeof column === "object" && Array.isArray((column as any).widgets) + ? (column as any).widgets + : []; + for (const [widgetIndex, widget] of columnWidgets.entries()) { + if (!widget || typeof widget !== "object") continue; + widgets.push({ + pageIndex, + columnIndex, + widgetIndex, + type: String((widget as any).type || ""), + title: String((widget as any).title || (widget as any).type || "widget"), + }); + } + } + } + return widgets; +} + +export function updateMonitorSites( + parsed: Record, + options: { + pageIndex?: number; + columnIndex?: number; + widgetIndex?: number; + title?: string; + sites: GlanceMonitorSite[]; + replace?: boolean; + }, +): Record { + const next = structuredClone(parsed); + const pages = Array.isArray(next.pages) ? next.pages : []; + let target: any = null; + + if (typeof options.pageIndex === "number") { + const page = pages[options.pageIndex]; + const columns = page?.columns; + target = columns?.[options.columnIndex ?? 0]?.widgets?.[options.widgetIndex ?? 0]; + } else if (options.title) { + for (const page of pages) { + for (const column of page?.columns || []) { + for (const widget of column?.widgets || []) { + if (widget?.type === "monitor" && widget?.title === options.title) { + target = widget; + break; + } + } + } + } + } else { + for (const page of pages) { + for (const column of page?.columns || []) { + for (const widget of column?.widgets || []) { + if (widget?.type === "monitor") { + target = widget; + break; + } + } + } + } + } + + if (!target || target.type !== "monitor") { + throw new Error("monitor widget not found"); + } + + const cleaned = (options.sites || []) + .map((site) => ({ + title: String(site.title || "").trim(), + url: String(site.url || "").trim(), + ...(site.icon ? { icon: String(site.icon).trim() } : {}), + })) + .filter((site) => site.title && site.url); + + if (options.replace !== false) { + target.sites = cleaned; + } else { + const existing = Array.isArray(target.sites) ? target.sites : []; + target.sites = [...existing, ...cleaned]; + } + + return next; +} + +export function parsedToYaml(parsed: Record): string { + return YAML.dump(parsed, { lineWidth: 120, noRefs: true }); +} diff --git a/plugins/glance/manifest.json b/plugins/glance/manifest.json new file mode 100644 index 000000000..0c5079a24 --- /dev/null +++ b/plugins/glance/manifest.json @@ -0,0 +1,29 @@ +{ + "manifestVersion": 1, + "id": "glance", + "name": "Glance", + "version": "0.1.0", + "description": "小安内置 Glance 仪表板:管理 glance.yml,读写监控项与小组件。", + "trust": "full-access", + "hidden": false, + "activationEvents": ["onStartup"], + "contributes": { + "page": { + "title": { + "zh": "概览", + "en": "Glance" + }, + "route": "/page", + "icon": "" + }, + "configuration": { + "properties": { + "serverUrl": { + "type": "string", + "title": "Glance 服务地址", + "default": "http://127.0.0.1:8080" + } + } + } + } +} diff --git a/plugins/glance/routes/api.ts b/plugins/glance/routes/api.ts new file mode 100644 index 000000000..1429aeacf --- /dev/null +++ b/plugins/glance/routes/api.ts @@ -0,0 +1,81 @@ +import { + listGlanceWidgets, + parsedToYaml, + readGlanceConfig, + updateMonitorSites, + writeGlanceConfig, +} from "../lib/glance-config.ts"; + +function pluginDataDir(ctx: any): string { + return ctx?.dataDir || ""; +} + +export default function registerGlanceRoutes(app: any, ctx: any) { + app.get("/config", (c: any) => { + try { + const { yaml, parsed } = readGlanceConfig(pluginDataDir(ctx)); + const serverUrl = String(ctx.config?.get?.("serverUrl") || "http://127.0.0.1:8080"); + return c.json({ + yaml, + widgets: listGlanceWidgets(parsed), + serverUrl, + configPath: "glance.yml", + }); + } catch (err: any) { + return c.json({ error: err.message }, 500); + } + }); + + app.put("/config", async (c: any) => { + try { + const body = await c.req.json(); + const yaml = typeof body?.yaml === "string" ? body.yaml : ""; + if (!yaml.trim()) return c.json({ error: "yaml is required" }, 400); + const saved = writeGlanceConfig(pluginDataDir(ctx), yaml); + return c.json({ + ok: true, + yaml: saved.yaml, + widgets: listGlanceWidgets(saved.parsed), + }); + } catch (err: any) { + return c.json({ error: err.message }, 400); + } + }); + + app.put("/settings/server-url", async (c: any) => { + try { + const body = await c.req.json(); + const serverUrl = String(body?.serverUrl || "").trim(); + if (!serverUrl) return c.json({ error: "serverUrl is required" }, 400); + ctx.config?.set?.("serverUrl", serverUrl); + return c.json({ ok: true, serverUrl }); + } catch (err: any) { + return c.json({ error: err.message }, 400); + } + }); + + app.post("/monitor/sites", async (c: any) => { + try { + const body = await c.req.json(); + const { yaml, parsed } = readGlanceConfig(pluginDataDir(ctx)); + const next = updateMonitorSites(parsed, { + pageIndex: body?.pageIndex, + columnIndex: body?.columnIndex, + widgetIndex: body?.widgetIndex, + title: body?.title, + sites: Array.isArray(body?.sites) ? body.sites : [], + replace: body?.replace !== false, + }); + const nextYaml = parsedToYaml(next); + const saved = writeGlanceConfig(pluginDataDir(ctx), nextYaml); + return c.json({ + ok: true, + yaml: saved.yaml, + widgets: listGlanceWidgets(saved.parsed), + previousYaml: yaml, + }); + } catch (err: any) { + return c.json({ error: err.message }, 400); + } + }); +} diff --git a/plugins/glance/routes/page.js b/plugins/glance/routes/page.js new file mode 100644 index 000000000..40c58742d --- /dev/null +++ b/plugins/glance/routes/page.js @@ -0,0 +1,41 @@ +export default function registerGlancePage(app, ctx) { + app.get("/page", (c) => c.html(renderShell(c, ctx))); +} + +function renderShell(c, ctx) { + const hanaCss = c.req.query("hana-css") || ""; + const theme = c.req.query("hana-theme") || "inherit"; + const base = `/api/plugins/${ctx.pluginId}`; + const assetSearch = buildAssetSearch(c); + + return ` + + + + + ${hanaCss ? `` : ""} + + + +
+ + +`; +} + +function buildAssetSearch(c) { + const params = new URLSearchParams(); + for (const key of ["token", "pluginIframeTicket", "pluginSurfaceSession", "agentId"]) { + const value = c.req.query(key); + if (value) params.set(key, value); + } + const query = params.toString(); + return query ? `?${query}` : ""; +} + +function escapeAttr(value) { + return String(value) + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/ `${w.type}:${w.title}`).join(", ") || "none"}`, + "", + yaml, + ].join("\n"), + }], + details: { serverUrl, widgets, yaml }, + }; +} diff --git a/plugins/glance/tools/update-monitor.ts b/plugins/glance/tools/update-monitor.ts new file mode 100644 index 000000000..e1231cd66 --- /dev/null +++ b/plugins/glance/tools/update-monitor.ts @@ -0,0 +1,76 @@ +import { + listGlanceWidgets, + parsedToYaml, + readGlanceConfig, + writeGlanceConfig, + updateMonitorSites, +} from "../lib/glance-config.ts"; + +export const name = "update-monitor"; +export const description = "Update monitor widget sites in glance.yml (service reachability list)."; + +export const sessionPermission = { + kind: "external_side_effect", + describeSideEffect: (input: any = {}) => ({ + kind: "config_write", + summary: `Update Glance monitor widget${input.title ? ` "${input.title}"` : ""}.`, + ruleId: "glance-update-monitor", + }), +}; + +export const parameters = { + type: "object", + properties: { + title: { + type: "string", + description: "Monitor widget title. Omit to update the first monitor widget.", + }, + pageIndex: { type: "number" }, + columnIndex: { type: "number" }, + widgetIndex: { type: "number" }, + replace: { + type: "boolean", + description: "Replace all sites when true (default). Append when false.", + }, + sites: { + type: "array", + description: "Monitor sites to show in the widget.", + items: { + type: "object", + properties: { + title: { type: "string" }, + url: { type: "string" }, + icon: { type: "string" }, + }, + required: ["title", "url"], + }, + }, + }, + required: ["sites"], +}; + +export async function execute(params: any, ctx: any) { + const dataDir = ctx?.dataDir; + if (!dataDir) throw new Error("Glance data directory unavailable"); + const { parsed } = readGlanceConfig(dataDir); + const next = updateMonitorSites(parsed, { + title: params?.title, + pageIndex: params?.pageIndex, + columnIndex: params?.columnIndex, + widgetIndex: params?.widgetIndex, + sites: Array.isArray(params?.sites) ? params.sites : [], + replace: params?.replace !== false, + }); + const yaml = parsedToYaml(next); + const saved = writeGlanceConfig(dataDir, yaml); + return { + content: [{ + type: "text", + text: `Monitor widget updated (${(params?.sites || []).length} site(s)).`, + }], + details: { + yaml: saved.yaml, + widgets: listGlanceWidgets(saved.parsed), + }, + }; +} diff --git a/plugins/glance/tools/write-config.ts b/plugins/glance/tools/write-config.ts new file mode 100644 index 000000000..148a57588 --- /dev/null +++ b/plugins/glance/tools/write-config.ts @@ -0,0 +1,48 @@ +import { + listGlanceWidgets, + readGlanceConfig, + writeGlanceConfig, +} from "../lib/glance-config.ts"; + +export const name = "write-config"; +export const description = "Replace the local Glance glance.yml configuration. Use glance_read-config first when unsure."; + +export const sessionPermission = { + kind: "external_side_effect", + describeSideEffect: () => ({ + kind: "config_write", + summary: "Update Glance dashboard configuration (glance.yml).", + ruleId: "glance-write-config", + }), +}; + +export const parameters = { + type: "object", + properties: { + yaml: { + type: "string", + description: "Full glance.yml content compatible with glanceapp/glance.", + }, + }, + required: ["yaml"], +}; + +export async function execute(params: { yaml?: string }, ctx: any) { + const dataDir = ctx?.dataDir; + if (!dataDir) throw new Error("Glance data directory unavailable"); + const yaml = String(params?.yaml || ""); + if (!yaml.trim()) throw new Error("yaml is required"); + const previous = readGlanceConfig(dataDir).yaml; + const saved = writeGlanceConfig(dataDir, yaml); + return { + content: [{ + type: "text", + text: `Glance config updated (${listGlanceWidgets(saved.parsed).length} widgets).`, + }], + details: { + previousYaml: previous, + yaml: saved.yaml, + widgets: listGlanceWidgets(saved.parsed), + }, + }; +} diff --git a/plugins/media/manifest.json b/plugins/media/manifest.json index 63fa43fe4..4a5a0811c 100644 --- a/plugins/media/manifest.json +++ b/plugins/media/manifest.json @@ -2,7 +2,7 @@ "id": "media", "name": "Media Generation", "version": "0.1.0", - "description": "Agent-facing media generation tools backed by Hana Media Manager.", + "description": "Xiaoan media generation capability for images and videos.", "trust": "full-access", "hidden": true } diff --git a/plugins/media/skills/media-generation-guide/SKILL.md b/plugins/media/skills/media-generation-guide/SKILL.md index faf6b27a9..539d74dce 100644 --- a/plugins/media/skills/media-generation-guide/SKILL.md +++ b/plugins/media/skills/media-generation-guide/SKILL.md @@ -1,6 +1,6 @@ --- name: media-generation-guide -description: Use Hana Media Generation tools for image and video generation. Read before calling media_generate-image, media_generate-video, or media_describe-options. +description: Use Xiaoan media generation tools for image and video generation. Read before calling media_generate-image, media_generate-video, or media_describe-options. --- # Media Generation @@ -45,5 +45,5 @@ Use this side-effect-free tool only when the user asks for a specific provider/m - Use only the `media_*` tools for image and video generation. - Do not silently switch providers after a failure. Report the error unless the user asks you to try another provider. -- Provider capability comes from Hana Media Provider Registry, not from chat model names. +- Provider capability comes from Xiaoan's media provider registry, not from chat model names. - If the user wants text inside the image, put the exact text in double quotes inside the prompt. diff --git a/plugins/xiaoan-core/README.md b/plugins/xiaoan-core/README.md index 2c2d715d3..1a2078137 100644 --- a/plugins/xiaoan-core/README.md +++ b/plugins/xiaoan-core/README.md @@ -1,6 +1,6 @@ # xiaoan-core -小安 Agent 核心插件:方法论工具 + 方向智能体模板 + 工作区文件模板。 +小安核心能力插件:方法论工具 + 方向智能体模板 + 工作区文件模板。 ## 工具 diff --git a/plugins/xiaoan-core/templates/AGENTS.xiaoan.md b/plugins/xiaoan-core/templates/AGENTS.xiaoan.md index bb268a698..f4ee1ea5d 100644 --- a/plugins/xiaoan-core/templates/AGENTS.xiaoan.md +++ b/plugins/xiaoan-core/templates/AGENTS.xiaoan.md @@ -1,6 +1,6 @@ # 小安工作区约定(AGENTS.md 模板) -> 放在项目根目录或书桌工作区,OpenHanako 会在启用 `inject_agents_md` 时注入。 +> 放在项目根目录或书桌工作区,小安会在启用工作区说明注入时读取。 ## 项目目标 @@ -15,7 +15,7 @@ ## 禁止 - 未经确认删除或覆盖用户文件 -- 对外发送消息(邮件、Bridge)未经 confirm +- 对外发送消息(邮件、渠道连接)未经 confirm - 假装右栏/书桌已有不存在的产出 ## Pipeline diff --git a/plugins/xiaoan-core/templates/SOUL.xiaoan.md b/plugins/xiaoan-core/templates/SOUL.xiaoan.md index f5da9378b..14a4ca7ab 100644 --- a/plugins/xiaoan-core/templates/SOUL.xiaoan.md +++ b/plugins/xiaoan-core/templates/SOUL.xiaoan.md @@ -1,6 +1,6 @@ # SOUL — 小安(模板) -> 对应 OpenClaw `SOUL.md` / OpenHanako `identity.md` + `ishiki.md` +> 对应小安状态的身份、语气与做事方式设定。 你是小安,桌面 AI 副驾驶。 diff --git a/scripts/build-server.mjs b/scripts/build-server.mjs index ff4e22323..860e43fba 100644 --- a/scripts/build-server.mjs +++ b/scripts/build-server.mjs @@ -12,8 +12,9 @@ * * 产出结构: * dist-server/{platform}-{arch}/ - * hana-server ← shell wrapper(设置 HANA_ROOT 并启动) - * hana ← shell wrapper(server-first CLI) + * xiaoan-server ← shell wrapper(设置 HANA_ROOT 并启动) + * xiaoan ← shell wrapper(server-first CLI) + * hana-server / hana ← legacy wrapper aliases * node ← Node.js runtime * bundle/ ← Vite bundle 产出 * index.js ← 入口(~750KB) @@ -147,10 +148,13 @@ if (!fs.existsSync(cachedNodeBin)) { } // 复制 node 二进制到 dist -// Windows 上改名为 hana-server.exe,让 main.cjs 的 bundled server 检测能命中, -// 同时在任务管理器中显示为 hana-server.exe 而非 node.exe(便于 NSIS 安装脚本按名杀进程) -const destNode = path.join(outDir, isWin ? "hana-server.exe" : "node"); +// Windows 上 canonical 改名为 xiaoan-server.exe,让任务管理器和诊断显示小安; +// 同时保留 hana-server.exe 作为 legacy 兼容入口,便于旧安装器/脚本平滑过渡。 +const destNode = path.join(outDir, isWin ? "xiaoan-server.exe" : "node"); fs.copyFileSync(cachedNodeBin, destNode); +if (isWin) { + fs.copyFileSync(cachedNodeBin, path.join(outDir, "hana-server.exe")); +} if (!isWin) fs.chmodSync(destNode, 0o755); console.log("[build-server] Node.js runtime ready"); @@ -611,17 +615,24 @@ fs.writeFileSync( // ── 10. Wrapper 脚本 ── if (isWin) { + fs.writeFileSync( + path.join(outDir, "xiaoan-server.cmd"), + '@echo off\r\nset "HANA_ROOT=%~dp0"\r\nset "HANA_SERVER_ENTRY=%~dp0bundle\\index.js"\r\n"%~dp0xiaoan-server.exe" "%~dp0bootstrap.js" %*\r\n', + ); + fs.writeFileSync( + path.join(outDir, "xiaoan.cmd"), + '@echo off\r\nset "HANA_ROOT=%~dp0"\r\nset "HANA_SERVER_ENTRY=%~dp0bundle\\index.js"\r\n"%~dp0xiaoan-server.exe" "%~dp0bundle\\cli.js" %*\r\n', + ); fs.writeFileSync( path.join(outDir, "hana-server.cmd"), - '@echo off\r\nset "HANA_ROOT=%~dp0"\r\nset "HANA_SERVER_ENTRY=%~dp0bundle\\index.js"\r\n"%~dp0hana-server.exe" "%~dp0bootstrap.js" %*\r\n', + '@echo off\r\nset "HANA_ROOT=%~dp0"\r\nset "HANA_SERVER_ENTRY=%~dp0bundle\\index.js"\r\n"%~dp0xiaoan-server.exe" "%~dp0bootstrap.js" %*\r\n', ); fs.writeFileSync( path.join(outDir, "hana.cmd"), - '@echo off\r\nset "HANA_ROOT=%~dp0"\r\nset "HANA_SERVER_ENTRY=%~dp0bundle\\index.js"\r\n"%~dp0hana-server.exe" "%~dp0bundle\\cli.js" %*\r\n', + '@echo off\r\nset "HANA_ROOT=%~dp0"\r\nset "HANA_SERVER_ENTRY=%~dp0bundle\\index.js"\r\n"%~dp0xiaoan-server.exe" "%~dp0bundle\\cli.js" %*\r\n', ); } else { - const wrapper = path.join(outDir, "hana-server"); - fs.writeFileSync(wrapper, [ + const serverWrapperLines = [ "#!/bin/sh", 'DIR="$(cd "$(dirname "$0")" && pwd)"', 'export HANA_ROOT="$DIR"', @@ -633,19 +644,26 @@ if (isWin) { "ulimit -n 65536 2>/dev/null || ulimit -n 8192 2>/dev/null || true", 'exec "$DIR/node" "$DIR/bootstrap.js" "$@"', "", - ].join("\n")); - fs.chmodSync(wrapper, 0o755); + ].join("\n"); + for (const name of ["xiaoan-server", "hana-server"]) { + const wrapper = path.join(outDir, name); + fs.writeFileSync(wrapper, serverWrapperLines); + fs.chmodSync(wrapper, 0o755); + } - const cliWrapper = path.join(outDir, "hana"); - fs.writeFileSync(cliWrapper, [ + const cliWrapperLines = [ "#!/bin/sh", 'DIR="$(cd "$(dirname "$0")" && pwd)"', 'export HANA_ROOT="$DIR"', 'export HANA_SERVER_ENTRY="$DIR/bundle/index.js"', 'exec "$DIR/node" "$DIR/bundle/cli.js" "$@"', "", - ].join("\n")); - fs.chmodSync(cliWrapper, 0o755); + ].join("\n"); + for (const name of ["xiaoan", "hana"]) { + const cliWrapper = path.join(outDir, name); + fs.writeFileSync(cliWrapper, cliWrapperLines); + fs.chmodSync(cliWrapper, 0o755); + } } console.log("[build-server] wrapper created"); diff --git a/scripts/check-node-version.cjs b/scripts/check-node-version.cjs new file mode 100644 index 000000000..a5ebbb7ba --- /dev/null +++ b/scripts/check-node-version.cjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node +// Guard: An-Agent 必须跑在 .nvmrc 指定的 Node 主版本(当前 24)。 +// 更高版本(如 26)下 better-sqlite3 等 native 模块会 ABI 不匹配,npm test 报大量假失败。 +// package.json engines 已声明 ">=24.12.0 <25",但 npm 默认只 warn 不强制;这里运行时硬拦。 +const fs = require("node:fs"); +const path = require("node:path"); + +const nvmrc = path.join(__dirname, "..", ".nvmrc"); +let expected; +try { + const m = fs + .readFileSync(nvmrc, "utf8") + .trim() + .match(/v?(\d+)/); + expected = m ? Number(m[1]) : null; +} catch { + process.exit(0); // 无 .nvmrc 不阻断,交给下游 +} + +const actual = Number(process.versions.node.split(".")[0]); +if (expected && actual !== expected) { + console.error( + [ + "", + `[check-node-version] 需要 Node v${expected}.x(见 .nvmrc),当前 v${process.versions.node}。`, + `native 模块(better-sqlite3 等)按 v${expected} ABI 编译,版本不符会让 npm test 报假失败。`, + `切换: fnm use ${expected} 或 nvm use ${expected}`, + "", + ].join("\n"), + ); + process.exit(1); +} diff --git a/scripts/dev-env.js b/scripts/dev-env.js index b60956580..e90c5e25f 100644 --- a/scripts/dev-env.js +++ b/scripts/dev-env.js @@ -2,13 +2,19 @@ import { homedir } from "node:os"; import { join } from "node:path"; export function defaultDevHanaHome() { - return join(homedir(), ".hanako-dev"); + return defaultDevXiaoanHome(); +} + +export function defaultDevXiaoanHome() { + return join(homedir(), ".xiaoan-dev"); } export function applyDevEnvironment(env = process.env, { nodeBin = process.execPath, } = {}) { - env.HANA_HOME = defaultDevHanaHome(); + const home = env.XIAOAN_HOME || env.HANA_HOME || defaultDevXiaoanHome(); + env.XIAOAN_HOME = home; + env.HANA_HOME = home; env.HANA_DEV_NODE_BIN = nodeBin; return env; } diff --git a/scripts/dev-web.js b/scripts/dev-web.js index 58543acbf..e6be484dd 100644 --- a/scripts/dev-web.js +++ b/scripts/dev-web.js @@ -4,7 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { applyDevEnvironment, defaultDevHanaHome } from "./dev-env.js"; +import { applyDevEnvironment, defaultDevXiaoanHome } from "./dev-env.js"; import { buildDevWebClientConfig, buildDevWebPreviewUrl, @@ -14,7 +14,7 @@ import { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(__dirname, ".."); -const hanaHome = defaultDevHanaHome(); +const hanaHome = process.env.XIAOAN_HOME || process.env.HANA_HOME || defaultDevXiaoanHome(); const serverInfoPath = path.join(hanaHome, "server-info.json"); let serverProcess = null; @@ -127,6 +127,7 @@ try { const clientConfig = buildDevWebClientConfig(serverInfo); spawnVite(clientConfig, serverInfo); log(`open ${buildDevWebPreviewUrl()}`); + log('after Vite is ready: npm run smoke:renderer'); } catch (err) { shutdown(); console.error(`[dev-web] ${err?.stack || err?.message || String(err)}`); diff --git a/scripts/notarize.cjs b/scripts/notarize.cjs index ee8f93200..db7d7cc78 100644 --- a/scripts/notarize.cjs +++ b/scripts/notarize.cjs @@ -1,19 +1,24 @@ -const { notarize } = require('@electron/notarize'); +const { notarize } = require("@electron/notarize"); exports.default = async function notarizing(context) { const { electronPlatformName, appOutDir } = context; - if (electronPlatformName !== 'darwin') return; - if (process.env.SKIP_NOTARIZE === 'true') { - console.log('Skipping notarization (SKIP_NOTARIZE=true)'); + if (electronPlatformName !== "darwin") return; + if (process.env.SKIP_NOTARIZE === "true") { + console.log("Skipping notarization (SKIP_NOTARIZE=true)"); return; } const appName = context.packager.appInfo.productFilename; console.log(`Notarizing ${appName}...`); - const password = process.env.APPLE_APP_SPECIFIC_PASSWORD || process.env.APPLE_ID_PASSWORD; - if (!password) { - throw new Error('Set APPLE_APP_SPECIFIC_PASSWORD or APPLE_ID_PASSWORD for notarization'); + const password = + process.env.APPLE_APP_SPECIFIC_PASSWORD || process.env.APPLE_ID_PASSWORD; + const appleId = process.env.APPLE_ID; + if (!password || !appleId) { + console.log( + `Skipping notarization for ${appName} (set APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD to enable; or SKIP_NOTARIZE=true)`, + ); + return; } await notarize({ @@ -23,5 +28,5 @@ exports.default = async function notarizing(context) { teamId: process.env.APPLE_TEAM_ID, }); - console.log('Notarization complete.'); + console.log("Notarization complete."); }; diff --git a/scripts/smoke-renderer.mjs b/scripts/smoke-renderer.mjs new file mode 100644 index 000000000..6f3ba02b9 --- /dev/null +++ b/scripts/smoke-renderer.mjs @@ -0,0 +1,132 @@ +#!/usr/bin/env node +/** + * Renderer smoke check — catches "white screen" class failures before hand-off. + * + * Usage: + * node scripts/smoke-renderer.mjs # dev server (SMOKE_RENDERER_URL) + * node scripts/smoke-renderer.mjs --dist # built dist-renderer/index.html + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, '..'); +const useDist = process.argv.includes('--dist'); + +const FORBIDDEN_BUNDLE_PATTERNS = [ + { pattern: /from\s*["']\.\/ChatArea["']/, label: 'ChatPage → ./ChatArea (broken import)' }, + { pattern: /releaseAppShellMotion/, label: 'motion failsafe (expected in bundle)' }, +]; + +function fail(message) { + console.error(`[smoke-renderer] FAIL ${message}`); + process.exit(1); +} + +function ok(message) { + console.log(`[smoke-renderer] OK ${message}`); +} + +function checkHtml(html, label) { + if (!html.includes('id="react-root"') && !html.includes("id='react-root'")) { + fail(`${label}: missing #react-root`); + } + if (!/main\.tsx|main-[A-Za-z0-9_-]+\.js/.test(html)) { + fail(`${label}: missing renderer entry script`); + } + ok(label); +} + +function resolveScriptPath(html, assetsDir) { + const moduleMatch = html.match(/src="\.?\/?assets\/(main-[^"]+\.js)"/); + if (moduleMatch) return path.join(assetsDir, moduleMatch[1]); + const devMatch = html.match(/src="\/src\/main\.tsx"/); + if (devMatch) return null; + fail('could not resolve renderer entry from HTML'); +} + +function checkBundles(assetsDir, html) { + if (!fs.existsSync(assetsDir)) fail(`missing assets dir: ${assetsDir}`); + + const mainBundles = fs.readdirSync(assetsDir).filter((name) => /^main-.*\.js$/.test(name)); + if (!mainBundles.length) fail('dist-renderer/assets: no main-*.js bundle'); + + const entryPath = resolveScriptPath(html, assetsDir); + const bundlesToScan = new Set(mainBundles); + if (entryPath && fs.existsSync(entryPath)) { + bundlesToScan.add(path.basename(entryPath)); + } + + const chatPageBundle = fs.readdirSync(assetsDir).find((name) => /^ChatPage-.*\.js$/.test(name)); + if (chatPageBundle) bundlesToScan.add(chatPageBundle); + + for (const name of bundlesToScan) { + const filePath = path.join(assetsDir, name); + if (!fs.existsSync(filePath)) continue; + const source = fs.readFileSync(filePath, 'utf8'); + + if (FORBIDDEN_BUNDLE_PATTERNS[0].pattern.test(source)) { + fail(`${name}: ${FORBIDDEN_BUNDLE_PATTERNS[0].label}`); + } + ok(`scan ${name}`); + } + + const allJs = fs.readdirSync(assetsDir).filter((name) => name.endsWith('.js')); + let combined = ''; + for (const name of allJs) { + combined += fs.readFileSync(path.join(assetsDir, name), 'utf8'); + } + + if (!/app-shell/.test(combined)) { + fail('renderer bundles missing app-shell marker'); + } + if (!/restored app-shell|data-chat-shell/.test(combined)) { + fail('renderer bundles missing motion failsafe markers'); + } + ok('motion failsafe present in bundle'); +} + +async function smokeDev() { + const url = process.env.SMOKE_RENDERER_URL || 'http://127.0.0.1:5173/index.html'; + let res; + try { + res = await fetch(url, { redirect: 'follow' }); + } catch (err) { + fail(`dev fetch ${url}: ${err instanceof Error ? err.message : String(err)} (run: npm run dev:web)`); + } + if (!res.ok) fail(`dev HTTP ${res.status} for ${url}`); + const html = await res.text(); + checkHtml(html, url); + + const moduleMatch = html.match(/src="(\/(?:src\/)?main\.tsx[^"]*)"/); + if (moduleMatch) { + const moduleUrl = new URL(moduleMatch[1], url).toString(); + const modRes = await fetch(moduleUrl); + if (!modRes.ok) fail(`dev module ${moduleUrl} HTTP ${modRes.status}`); + const modSrc = await modRes.text(); + if (/from\s*["']\.\/ChatArea["']/.test(modSrc)) { + fail('dev ChatPage still imports ./ChatArea'); + } + ok(`dev module ${moduleMatch[1].split('?')[0]}`); + } else { + ok('dev html (no main.tsx — using built assets)'); + } +} + +function smokeDist() { + const distDir = path.join(rootDir, 'desktop', 'dist-renderer'); + const indexPath = path.join(distDir, 'index.html'); + if (!fs.existsSync(indexPath)) { + fail(`missing ${path.relative(rootDir, indexPath)} — run npm run build:renderer first`); + } + const html = fs.readFileSync(indexPath, 'utf8'); + checkHtml(html, path.relative(rootDir, indexPath)); + checkBundles(path.join(distDir, 'assets'), html); +} + +if (useDist) { + smokeDist(); +} else { + await smokeDev(); +} diff --git a/scripts/sync-docs-to-obsidian.sh b/scripts/sync-docs-to-obsidian.sh new file mode 100755 index 000000000..9422ad9b6 --- /dev/null +++ b/scripts/sync-docs-to-obsidian.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# 将 An-Agent 仓库说明与代码地图同步到黑曜石知识库。 +# 用法(在仓库根目录): +# ./scripts/sync-docs-to-obsidian.sh +# OBSIDIAN_AN_AGENT_DIR=~/Obsidian/初始/99\ -\ Agent指南与规范/An-Agent ./scripts/sync-docs-to-obsidian.sh + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OBS="${OBSIDIAN_AN_AGENT_DIR:-$HOME/Obsidian/初始/99 - Agent指南与规范/An-Agent}" + +mkdir -p \ + "$OBS/docs/agent-design" \ + "$OBS/docs/incidents" \ + "$OBS/代码地图" \ + "$OBS/工程与维护" \ + "$OBS/Agent设计" \ + "$OBS/仓库根说明" + +echo "Sync: $REPO_ROOT -> $OBS" + +cp -R "$REPO_ROOT/docs/"* "$OBS/docs/" + +for f in CODEMAP.md CODEMAP-DEPENDENCIES.md CODEMAP-DESKTOP-UI.md CODEMAP-XIAOAN-LAYER.md \ + CODEMAP-SERVER-ROUTES.md CODEMAP-CORE-HUB-LIB.md CODEMAP-REFERENCE-ABSORB.md \ + HANAKO-INTERNALIZATION.md; do + cp -f "$REPO_ROOT/docs/$f" "$OBS/代码地图/$f" +done + +for f in XIAOAN-ENGINEERING-METHODOLOGY.md XIAOAN-CODEBASE-INTERNALIZATION.md \ + XIAOAN-RUNTIME-CONTRACT.md XIAOAN-LOCAL-DEV-CONTEXT.md XIAOAN-DISTRIBUTION.md \ + FORK-CONFIG.md MAINTAINER.md LOCAL-DEV.md XIAOAN-LAYOUT.md \ + XIAOAN-DESKTOP-UI-STANDARDS.md DEPENDENCY-SECURITY-TRIAGE.md; do + cp -f "$REPO_ROOT/docs/$f" "$OBS/工程与维护/$f" +done + +cp -f "$REPO_ROOT/docs/agent-design/"*.md "$OBS/Agent设计/" + +for f in README.md README_EN.md CONTRIBUTING.md UPSTREAM-NOTICE.md PLUGINS.md PLUGINS_EN.md \ + PLUGIN_SDK.md SECURITY.md CODE_OF_CONDUCT.md CLAUDE.md AGENTS.md; do + if [[ -f "$REPO_ROOT/$f" ]]; then + cp -f "$REPO_ROOT/$f" "$OBS/仓库根说明/$f" + fi +done + +# 黑曜石专属规范(若仓库有更新则覆盖工程与维护副本) +if [[ -f "$OBS/小安桌面UI-工程化配置与开发规范.md" ]]; then + cp -f "$OBS/小安桌面UI-工程化配置与开发规范.md" "$OBS/工程与维护/小安桌面UI-工程化配置与开发规范.md" 2>/dev/null || true +fi + +COUNT="$(find "$OBS" -name '*.md' | wc -l | tr -d ' ')" +echo "Done. $COUNT markdown files under An-Agent/" diff --git a/server/bootstrap.ts b/server/bootstrap.ts index 7e5a23489..541606311 100644 --- a/server/bootstrap.ts +++ b/server/bootstrap.ts @@ -18,7 +18,7 @@ const hanaRoot = process.env.HANA_ROOT || import.meta.dirname; const serverEntry = process.env.HANA_SERVER_ENTRY || path.join(hanaRoot, "bundle", "index.js"); log(`[server-bootstrap] process started pid=${process.pid} platform=${process.platform} arch=${process.arch}`); -log(`[server-bootstrap] node=${process.version} hanaHome=${process.env.HANA_HOME || "unset"}`); +log(`[server-bootstrap] node=${process.version} xiaoanHome=${process.env.XIAOAN_HOME || process.env.HANA_HOME || "unset"}`); log(`[server-bootstrap] root=${hanaRoot}`); log(`[server-bootstrap] entry=${serverEntry}`); diff --git a/server/http/route-security.ts b/server/http/route-security.ts index e20994e11..349a76bf0 100644 --- a/server/http/route-security.ts +++ b/server/http/route-security.ts @@ -121,6 +121,9 @@ export function classifyHttpRoute({ method = "GET", path = "" } = {}) { if (routePath === "/api/preferences/sidebar-ui") { return (verb === "GET" || verb === "PUT") ? scoped("chat") : LOCAL_ONLY; } + if (routePath === "/api/preferences/settings-ui") { + return (verb === "GET" || verb === "PUT") ? scoped("chat") : LOCAL_ONLY; + } if (routePath === "/api/preferences/session-permission-default") { return (verb === "GET" || verb === "PUT") ? scoped("chat") : LOCAL_ONLY; } diff --git a/server/index.ts b/server/index.ts index 52a287d4d..c1838260f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,5 +1,5 @@ /** - * HanaAgent Server — HTTP + WebSocket API + * Xiaoan Server — HTTP + WebSocket API * * 启动方式: * node server/index.js (独立运行) @@ -11,6 +11,7 @@ import crypto from "crypto"; import fs from "fs"; import { setMaxListeners } from "events"; import path from "path"; +import { getProductNameEn } from "../shared/xiaoan-fork.ts"; import { Hono } from "hono"; import { createAdaptorServer } from "@hono/node-server"; import { createNodeWebSocket } from "@hono/node-ws"; @@ -196,9 +197,10 @@ function createListenPermissionStartupError(cause: any, { host, port, listenHost return err; } -// 用户数据存放在 ~/.hanako/(打包后与产品代码分离) -// 开发时可通过 HANA_HOME 环境变量隔离数据目录,如:HANA_HOME=~/.hanako-dev node server/index.js -const hanakoHome = resolveHanakoHome(process.env.HANA_HOME); +// 用户数据存放在 ~/.xiaoan/(打包后与产品代码分离),兼容 legacy ~/.hanako。 +// 开发时优先通过 XIAOAN_HOME 隔离数据目录;HANA_HOME 仍作为 legacy alias 可用。 +const hanakoHome = resolveHanakoHome(process.env.XIAOAN_HOME || process.env.HANA_HOME); +process.env.XIAOAN_HOME = hanakoHome; process.env.HANA_HOME = hanakoHome; ensureHanaPiSdkDirs(hanakoHome); configureProcessPiSdkEnv(hanakoHome); @@ -1057,7 +1059,7 @@ try { const actualPort = address.port; serverRuntimeState.actualPort = actualPort; - log.log(`HanaAgent Server 运行在 http://${host}:${actualPort}`); + log.log(`${getProductNameEn()} Server 运行在 http://${host}:${actualPort}`); dlog.log("server", `listening on :${actualPort}`); // 写 server-info 文件,供 Electron 检测复用或外部工具查询。 diff --git a/server/routes/chat.ts b/server/routes/chat.ts index a140fdac5..97e45923b 100644 --- a/server/routes/chat.ts +++ b/server/routes/chat.ts @@ -4,6 +4,8 @@ * 桥接 Pi SDK streaming 事件 → WebSocket 消息 * 支持多 session 并发:所有 session 事件平等广播,前端按 sessionPath 路由 */ +import { Hono } from "hono"; +import { ThinkTagParser, MoodParser, CardParser } from "../../core/events.ts"; import { MINIAN_AGENT_NAMES } from "../../shared/xiaoan-defaults.ts"; import { extractBlocks } from "../block-extractors.ts"; import { normalizePluginChatSurfaceBlocks } from "../plugin-chat-surface.ts"; diff --git a/server/routes/preferences.ts b/server/routes/preferences.ts index 532b933b6..f46dc95bd 100644 --- a/server/routes/preferences.ts +++ b/server/routes/preferences.ts @@ -33,6 +33,7 @@ import { normalizeWorkspaceUiSurface, } from "../../shared/workspace-ui-state.ts"; import { normalizeSidebarUiPrefs, normalizeSidebarUiPrefsPatch } from "../../shared/sidebar-ui-state.ts"; +import { normalizeSettingsUiPrefs, normalizeSettingsUiPrefsPatch } from "../../shared/settings-ui-preferences.ts"; import { normalizeNotificationPreferences } from "../../shared/notification-preferences.ts"; import { normalizeQuickChatPreferences } from "../../shared/quick-chat-preferences.ts"; import { normalizeBrowserPreferences } from "../../shared/browser-preferences.ts"; @@ -467,6 +468,30 @@ export function createPreferencesRoute(engine: any, options: Record } }); + route.get("/preferences/settings-ui", async (c) => { + try { + return c.json({ settingsUi: engine.getSettingsUiPrefs?.() || normalizeSettingsUiPrefs({}) }); + } catch (err) { + return c.json({ error: err.message }, 500); + } + }); + + route.put("/preferences/settings-ui", async (c) => { + try { + const body = await safeJson(c); + if (!body || typeof body !== "object") { + return c.json({ error: "invalid JSON body" }, 400); + } + if (typeof engine.setSettingsUiPrefs !== "function") { + return c.json({ error: "settings UI preferences unavailable" }, 500); + } + const settingsUi = engine.setSettingsUiPrefs(normalizeSettingsUiPrefsPatch(body.settingsUi || body)); + return c.json({ ok: true, settingsUi }); + } catch (err) { + return c.json({ error: err.message }, 400); + } + }); + route.get("/preferences/plugin-ui", async (c) => { try { return c.json(engine.getPluginUiPrefs()); diff --git a/shared/builtin-plugins.ts b/shared/builtin-plugins.ts new file mode 100644 index 000000000..5e25a7427 --- /dev/null +++ b/shared/builtin-plugins.ts @@ -0,0 +1,16 @@ +/** 新用户默认顶栏插件顺序:频道旁优先展示内置 Glance */ +export const DEFAULT_PLUGIN_UI_TAB_ORDER = [ + "channels", + "plugin:glance", +] as const; + +export const BUILTIN_PLUGIN_IDS = [ + "glance", + "xiaoan-core", + "media", + "mcp", + "image-gen", + "beautify", + "office", + "jimeng-cli", +] as const; diff --git a/shared/settings-ui-preferences.ts b/shared/settings-ui-preferences.ts new file mode 100644 index 000000000..a3f1a2eb3 --- /dev/null +++ b/shared/settings-ui-preferences.ts @@ -0,0 +1,78 @@ +/** + * 设置 UI 偏好:开发者模式开关与核心 Tab 白名单。 + * + * @see shared/xiaoan-settings-product-notes.ts · XIAOAN_DEVELOPER_MODE_NOTES + * + * 注意:developerModeEnabled 仅门控桌面设置 UI,不门控服务端 config API。 + */ +export interface SettingsUiPrefs { + developerModeEnabled: boolean; +} + +export type SettingsUiPrefsPatch = Partial; + +export const DEVELOPER_UNLOCK_PASSPHRASE = "xiaoan"; + +/** 默认可见的设置页(非开发者模式) */ +export const CORE_SETTINGS_TAB_IDS = [ + "agent", + "me", + "interface", + "general", + "providers", + "about", +] as const; + +export type CoreSettingsTabId = (typeof CORE_SETTINGS_TAB_IDS)[number]; + +const CORE_TAB_ID_SET = new Set(CORE_SETTINGS_TAB_IDS); + +export function isCoreSettingsTab(tabId: string): tabId is CoreSettingsTabId { + return CORE_TAB_ID_SET.has(tabId); +} + +export function isSettingsTabVisible(tabId: string, developerModeEnabled: boolean): boolean { + if (developerModeEnabled) return true; + return isCoreSettingsTab(tabId); +} + +export function matchesDeveloperUnlockPassphrase(value: unknown): boolean { + return String(value || "").trim().toLowerCase() === DEVELOPER_UNLOCK_PASSPHRASE; +} + +export function normalizeSettingsUiPrefs(raw: unknown = {}): SettingsUiPrefs { + const source = raw && typeof raw === "object" && !Array.isArray(raw) + ? (raw as Record) + : {}; + return { + developerModeEnabled: source.developerModeEnabled === true, + }; +} + +export function normalizeSettingsUiPrefsPatch(raw: unknown = {}): SettingsUiPrefsPatch { + const source = raw && typeof raw === "object" && !Array.isArray(raw) + ? (raw as Record) + : {}; + const patch: SettingsUiPrefsPatch = {}; + if (Object.prototype.hasOwnProperty.call(source, "developerModeEnabled")) { + patch.developerModeEnabled = source.developerModeEnabled === true; + } + return patch; +} + +export function mergeSettingsUiPrefs(current: unknown = {}, partial: unknown = {}): SettingsUiPrefs { + const base = normalizeSettingsUiPrefs(current); + const patch = normalizeSettingsUiPrefsPatch(partial); + return normalizeSettingsUiPrefs({ ...base, ...patch }); +} + +export function getVisibleSettingsTabIds( + developerModeEnabled: boolean, + extraTabIds: string[] = [], +): Set { + const ids = new Set(CORE_SETTINGS_TAB_IDS); + if (developerModeEnabled) { + for (const id of extraTabIds) ids.add(id); + } + return ids; +} diff --git a/shared/storage-key-migrate.ts b/shared/storage-key-migrate.ts index 4aba8ab58..c18399e94 100644 --- a/shared/storage-key-migrate.ts +++ b/shared/storage-key-migrate.ts @@ -13,7 +13,7 @@ export function readMigratedStorageValue( } export function writeMigratedStorageValue( - storage: Pick, + storage: Pick, pair: StorageKeyPair, value: string, ): void { diff --git a/shared/xiaoan-builtin-relay.ts b/shared/xiaoan-builtin-relay.ts new file mode 100644 index 000000000..68fd1dd2a --- /dev/null +++ b/shared/xiaoan-builtin-relay.ts @@ -0,0 +1,81 @@ +/** + * 内置安芯跃动中转站(anxin-relay)配置真源与 URL 解析。 + * + * @see shared/xiaoan-fork.json · builtinRelay + * @see shared/xiaoan-settings-product-notes.ts · XIAOAN_BUILTIN_RELAY_NOTES + */ +import { XIAOAN_FORK } from "./xiaoan-fork.ts"; + +export const XIAOAN_BUILTIN_RELAY_PROVIDER_ID = + XIAOAN_FORK.builtinRelay?.providerId ?? "anxin-relay"; + +export function getBuiltinRelayDisplayName(isZh: boolean): string { + const relay = XIAOAN_FORK.builtinRelay; + if (isZh) return relay?.displayNameZh ?? "安芯跃动中转站"; + return relay?.displayNameEn ?? "Anxin Relay Hub"; +} + +export function getBuiltinRelayBaseUrl(): string { + const fromEnv = typeof process !== "undefined" ? process.env?.XIAOAN_RELAY_BASE_URL : undefined; + if (fromEnv && String(fromEnv).trim()) return String(fromEnv).trim(); + const fromFork = XIAOAN_FORK.builtinRelay?.defaultBaseUrl; + if (fromFork && String(fromFork).trim()) return String(fromFork).trim(); + return ""; +} + +export function getBuiltinRelayDefaultApi(): string { + return XIAOAN_FORK.builtinRelay?.defaultApi ?? "openai-completions"; +} + +export function getBuiltinRelayProviderDefaults(): { base_url: string; api: string } { + return { + base_url: getBuiltinRelayBaseUrl(), + api: getBuiltinRelayDefaultApi(), + }; +} + +export function getBuiltinRelayRechargeUrl(): string { + const fromEnv = typeof process !== "undefined" ? process.env?.XIAOAN_RELAY_RECHARGE_URL : undefined; + if (fromEnv && String(fromEnv).trim()) return String(fromEnv).trim(); + const fromFork = XIAOAN_FORK.builtinRelay?.rechargeUrl; + if (fromFork && String(fromFork).trim()) return String(fromFork).trim(); + return deriveRelayConsolePath("/console/topup"); +} + +export function getBuiltinRelayConsoleUrl(): string { + const fromEnv = typeof process !== "undefined" ? process.env?.XIAOAN_RELAY_CONSOLE_URL : undefined; + if (fromEnv && String(fromEnv).trim()) return String(fromEnv).trim(); + const fromFork = XIAOAN_FORK.builtinRelay?.consoleUrl; + if (fromFork && String(fromFork).trim()) return String(fromFork).trim(); + return deriveRelayConsolePath("/console"); +} + +export function getBuiltinRelayTokenUrl(): string { + const fromEnv = typeof process !== "undefined" ? process.env?.XIAOAN_RELAY_TOKEN_URL : undefined; + if (fromEnv && String(fromEnv).trim()) return String(fromEnv).trim(); + const fromFork = XIAOAN_FORK.builtinRelay?.tokenUrl; + if (fromFork && String(fromFork).trim()) return String(fromFork).trim(); + return deriveRelayConsolePath("/console/token"); +} + +function deriveRelayConsolePath(pathname: string): string { + const base = getBuiltinRelayBaseUrl(); + if (!base) return ""; + try { + return `${new URL(base).origin}${pathname}`; + } catch { + return ""; + } +} + +export function isBuiltinRelayProvider(providerId: unknown): boolean { + return String(providerId || "").trim() === XIAOAN_BUILTIN_RELAY_PROVIDER_ID; +} + +export function filterProvidersForBuiltinRelay>( + providers: T, +): Partial { + const id = XIAOAN_BUILTIN_RELAY_PROVIDER_ID; + if (!providers[id]) return { [id]: {} } as Partial; + return { [id]: providers[id] } as Partial; +} diff --git a/shared/xiaoan-defaults.ts b/shared/xiaoan-defaults.ts index 34def0ee1..637d5025e 100644 --- a/shared/xiaoan-defaults.ts +++ b/shared/xiaoan-defaults.ts @@ -18,11 +18,11 @@ export const XIAOAN_PRODUCT_NAMES = { } as const; /** - * 小小安 Minian — 原 Hana/Hanako 运行时 persona 的内化名(助手显示名默认值)。 + * 小安 Xiaoan — 原 Hana/Hanako 运行时 persona 的产品化显示名(助手显示名默认值)。 */ export const MINIAN_AGENT_NAMES = { - zh: "小小安", - en: "Minian", + zh: "小安", + en: "Xiaoan", } as const; /** @deprecated 助手默认名请用 MINIAN_AGENT_NAMES */ @@ -37,8 +37,10 @@ export const LEGACY_HANAKO_AGENT_NAMES = new Set([ "Hanako", "Hana", "花子", + "小小安", "HanaAgent", "HanakoAgent", + "Minian", ]); export function isLegacyHanakoDisplayName(name: unknown): boolean { diff --git a/shared/xiaoan-fork.cjs b/shared/xiaoan-fork.cjs index 03bd21834..c9e754439 100644 --- a/shared/xiaoan-fork.cjs +++ b/shared/xiaoan-fork.cjs @@ -13,11 +13,11 @@ function getProductNameZh() { } function getMinianNameEn() { - return fork.minianNameEn || "Minian"; + return fork.minianNameEn || "Xiaoan"; } function getMinianNameZh() { - return fork.minianName || "小小安"; + return fork.minianName || "小安"; } function getDefaultAgentName(isZh = false) { @@ -56,6 +56,26 @@ function getMacAppBundleName() { return fork.distribution?.macAppBundle || "Xiaoan.app"; } +function getProviderUserAgent() { + return fork.distribution?.userAgent || "Xiaoan/1.0"; +} + +function getBotUserAgent() { + return fork.distribution?.botUserAgent || "XiaoanBot/1.0"; +} + +function getProvidersYamlHeaderZh() { + return `# ${getProductNameZh()} 供应商配置(全局,跨 agent 共享)\n`; +} + +function getAgentConfigYamlHeaderZh() { + return `# ${getProductNameZh()} 助手配置\n# 由设置页面管理,手动编辑也可以\n\n`; +} + +function getSystemConfigYamlHeaderZh() { + return `# ${getProductNameZh()} 系统配置\n`; +} + module.exports = { fork, getProductNameEn, @@ -69,4 +89,9 @@ module.exports = { getDistributionUserAgent, getDistributionBotUserAgent, getMacAppBundleName, + getProviderUserAgent, + getBotUserAgent, + getProvidersYamlHeaderZh, + getAgentConfigYamlHeaderZh, + getSystemConfigYamlHeaderZh, }; diff --git a/shared/xiaoan-fork.json b/shared/xiaoan-fork.json index 77af69fc0..0824ba46c 100644 --- a/shared/xiaoan-fork.json +++ b/shared/xiaoan-fork.json @@ -3,15 +3,15 @@ "productName": "小安", "productNameEn": "Xiaoan", "minianId": "minian", - "minianName": "小小安", - "minianNameEn": "Minian", + "minianName": "小安", + "minianNameEn": "Xiaoan", "maintainer": "An-an-525", "repositoryUrl": "https://github.com/An-an-525/An-Agent", "upstreamName": "OpenHanako", "upstreamRepositoryUrl": "https://github.com/liliMozi/openhanako", "methodologyDoc": "docs/agent-design/XIAOAN-METHODOLOGY.md", "autoUpdate": { - "enabled": false, + "enabled": true, "githubOwner": "an-an-525", "githubRepo": "An-Agent" }, @@ -26,5 +26,15 @@ "pluginMarketplace": { "defaultIndexUrl": "https://raw.githubusercontent.com/liliMozi/OH-Plugins/main/marketplace.json", "upstreamLabel": "OH-Plugins (OpenHanako-compatible; override via XIAOAN_PLUGIN_MARKETPLACE_URL)" + }, + "builtinRelay": { + "providerId": "anxin-relay", + "displayNameZh": "安芯跃动中转站", + "displayNameEn": "Anxin Relay Hub", + "defaultBaseUrl": "https://an520.xin/v1", + "defaultApi": "openai-completions", + "rechargeUrl": "https://an520.xin/console/topup", + "consoleUrl": "https://an520.xin/console", + "tokenUrl": "https://an520.xin/console/token" } } diff --git a/shared/xiaoan-fork.ts b/shared/xiaoan-fork.ts index 647f5c0f2..a23500ed0 100644 --- a/shared/xiaoan-fork.ts +++ b/shared/xiaoan-fork.ts @@ -17,15 +17,15 @@ export function isAutoUpdateEnabled(): boolean { export function getPlatformIdentityPrompt(isZh: boolean): string { if (isZh) { return [ - `你运行在「${XIAOAN_FORK.productName}」桌面 Agent 上;助手 persona 为「${XIAOAN_FORK.minianName}」(Minian,原 Hana 内化)。`, - `底座:${XIAOAN_FORK.upstreamName} fork · Pi SDK 运行时。`, + `你运行在「${XIAOAN_FORK.productName}」桌面 AI 助手上;用户可见身份就是「${XIAOAN_FORK.minianName}」。`, + `底座继承自 ${XIAOAN_FORK.upstreamName},运行时为 Pi SDK;对用户保持小安产品口吻。`, `项目主页:${XIAOAN_FORK.repositoryUrl}`, `方法论:xiaoan-core 工具(think/brief/ask/plan/confirm)与右栏工作区真相一致。`, ].join("\n"); } return [ - `You run on ${XIAOAN_FORK.productNameEn} (${XIAOAN_FORK.minianNameEn} persona, internalized from legacy Hana).`, - `Stack: ${XIAOAN_FORK.upstreamName} fork · Pi SDK runtime.`, + `You run on ${XIAOAN_FORK.productNameEn}, a desktop AI assistant. The user-facing assistant identity is ${XIAOAN_FORK.minianNameEn}.`, + `Stack: inherited from ${XIAOAN_FORK.upstreamName} with Pi SDK runtime; keep the product voice as Xiaoan.`, `Project: ${XIAOAN_FORK.repositoryUrl}`, `Methodology: xiaoan-core tools (think/brief/ask/plan/confirm) and right-panel workspace truth.`, ].join("\n"); @@ -46,3 +46,35 @@ export function getUpdateFeed(): { provider: "github"; owner: string; repo: stri export function getDefaultPluginMarketplaceUrl(): string { return XIAOAN_FORK.pluginMarketplace.defaultIndexUrl; } + +export function getProviderUserAgent(): string { + return XIAOAN_FORK.distribution.userAgent; +} + +export function getBotUserAgent(): string { + return XIAOAN_FORK.distribution.botUserAgent; +} + +export function getProvidersYamlHeaderZh(): string { + return `# ${XIAOAN_FORK.productName} 供应商配置(全局,跨 agent 共享)\n`; +} + +export function getAgentConfigYamlHeaderZh(): string { + return `# ${XIAOAN_FORK.productName} 助手配置\n# 由设置页面管理,手动编辑也可以\n\n`; +} + +export function getSystemConfigYamlHeaderZh(): string { + return `# ${XIAOAN_FORK.productName} 系统配置\n`; +} + +export function getProductNameEn(): string { + return XIAOAN_FORK.productNameEn; +} + +export function getProductNameZh(): string { + return XIAOAN_FORK.productName; +} + +export function getWindowsExecutableNames(): readonly string[] { + return ["Xiaoan.exe", "HanaAgent.exe", "Hanako.exe"] as const; +} diff --git a/shared/xiaoan-settings-product-notes.ts b/shared/xiaoan-settings-product-notes.ts new file mode 100644 index 000000000..d604ed3f0 --- /dev/null +++ b/shared/xiaoan-settings-product-notes.ts @@ -0,0 +1,84 @@ +/** + * 小安设置 / 模型配置 — 产品层功能性注解(维护者向) + * + * 记录当前实现边界、已知问题与后续深度集成方向。 + * UI 用户可见文案见 desktop/src/locales/zh.json · settings.providers.builtinRelay / settings.developer + * 改行为时请先更新本文件与对应 locale,再改实现。 + */ + +/** 开发者模式:侧栏整页 + 各 Tab 内 DeveloperOnly 区块 */ +export const XIAOAN_DEVELOPER_MODE_NOTES = { + purpose: + "默认只暴露日常设置;高级项(人格提示词、多模型来源、实验功能等)需解锁开发者模式。", + unlock: + "关于小安 → 点击头像 → 隐藏输入框输入口令 xiaoan → Enter;无 UI 提示。关闭:关于页开关。", + persistence: "developerModeEnabled 存于 /api/preferences/settings-ui,仅影响桌面设置 UI。", + knownIssues: [ + "纯 UI 门控:服务端 /api/config 仍可写入自定义 provider,未按开发者模式拦截。", + "设置搜索仅按 developerOnly 标记过滤条目,无法搜到被 DeveloperOnly 包裹但索引未标记的字段。", + "开发者口令硬编码在 shared/settings-ui-preferences.ts,无轮换/账户绑定。", + "AgentTab 对话模型在开发者区块内,但聊天页 ModelSelector 仍展示全部 /api/models。", + "解锁后无「恢复默认简洁界面」引导,需用户手动关开关。", + ], + futureWork: [ + "服务端策略:非开发者模式拒绝非 anxin-relay 的 provider 写入。", + "设置搜索与 DeveloperOnly 区块自动对齐(或从 DOM/元数据生成索引)。", + "可选:开发者模式与中转站账号权限打通。", + ], +} as const; + +/** 内置安芯跃动中转站(anxin-relay / an520.xin) */ +export const XIAOAN_BUILTIN_RELAY_NOTES = { + purpose: + "非开发者模式下,模型配置唯一入口;配置真源 shared/xiaoan-fork.json · builtinRelay + 环境变量覆盖。", + providerId: "anxin-relay", + apiSurface: "OpenAI-compatible /v1;控制台为 New API(/console、/console/token、/console/topup)。", + knownIssues: [ + "登录、注册、创建令牌、充值均依赖 hana.openExternal 跳转浏览器,无应用内 OAuth / Cookie 共享。", + "用户需手动复制 API Key 回设置页,无法从中转站拉取或刷新令牌列表。", + "不展示余额、用量、套餐;充值后需用户自行确认是否恢复可用。", + "BuiltinRelayProvidersTab 隐藏 Base URL / Headers / API 协议编辑,但历史 config 中其它 provider 仍可能被引擎加载。", + "OtherModelsSection 仅过滤 providers 入参,globalModels 若已指向其它 provider 的 utility/vision 仍生效。", + "hideSearch 隐藏搜索服务配置,联网搜索可能仍走历史全局配置或 Auto。", + "anxin-relay 无专属 ProviderIcon,回退为通用占位图标。", + "defaultBaseUrl 依赖 fork 配置;部署忘记配置时控制台链接推导为空。", + "深度集成未做:会话保持、令牌过期提醒、余额不足内嵌充值、模型读取后的 utility/vision 推荐。", + ], + futureWork: [ + "OAuth 或 magic link 登录中转站,应用内展示余额与令牌管理。", + "保存 Key 后自动推荐 utility / vision 模型,并与聊天页模型列表策略一致。", + "引擎层:非开发者模式 resolveModel 仅允许 anxin-relay。", + "聊天页模型列表与设置页策略一致。", + "余额不足时应用内 Toast + 充值深链。", + ], + envOverrides: [ + "XIAOAN_RELAY_BASE_URL", + "XIAOAN_RELAY_CONSOLE_URL", + "XIAOAN_RELAY_TOKEN_URL", + "XIAOAN_RELAY_RECHARGE_URL", + ], +} as const; + +/** 各核心 Tab 内 DeveloperOnly 区块划分(与 settings-search-index developerOnly 应对齐) */ +export const XIAOAN_SETTINGS_SECTION_GATING = { + agent: { + core: ["助手卡片浏览与切换", "助手名称"], + developer: ["表达风格(元)", "对话模型", "身份/内在设定", "记忆", "经验", "工具", "卡片增删/导出"], + }, + me: { + core: ["头像", "昵称"], + developer: ["用户画像 profile 长文本"], + }, + interface: { + core: ["主题", "阅读字体预设", "语言/时区"], + developer: ["字号/聊天宽度滑块", "纸纹/落叶", "硬件加速", "侧栏行模式", "编辑器排版", "快捷键展示"], + }, + general: { + core: ["开机启动", "保持唤醒", "通知"], + developer: ["快捷对话全局热键"], + }, + providers: { + core: ["BuiltinRelayProvidersTab:中转站 Key + 工具模型(限 relay provider 列表)"], + developer: ["DeveloperProvidersTab:完整多来源 OAuth/API/用量"], + }, +} as const; diff --git a/shared/yuan-visuals.ts b/shared/yuan-visuals.ts index 014498e65..1f478caa6 100644 --- a/shared/yuan-visuals.ts +++ b/shared/yuan-visuals.ts @@ -25,16 +25,16 @@ export const YUAN_VISUALS: Readonly>> = Obje }), butter: Object.freeze({ yuan: "butter", - symbol: "❊", - moodLabel: "PULSE", - accent: "#5BA88C", + symbol: "❋", + moodLabel: "心绪", + accent: "#C47A6E", avatar: "Butter.png", }), ming: Object.freeze({ yuan: "ming", symbol: "◈", - moodLabel: "REFLECT", - accent: "#8BA4B4", + moodLabel: "沉思", + accent: "#4A5F7A", avatar: "Ming.png", }), }); diff --git a/tests/agent-master-session-decoupling.test.ts b/tests/agent-master-session-decoupling.test.ts index 9cf93f906..da02dede6 100644 --- a/tests/agent-master-session-decoupling.test.ts +++ b/tests/agent-master-session-decoupling.test.ts @@ -159,6 +159,27 @@ describe("agent.systemPrompt: master / per-session 解耦", () => { await agent.dispose(); }); + it("旧的 deep task 模型不可用时,记忆运行时回退到聊天模型", async () => { + const agent = makeAgent(agentsDir, tmpDir); + const resolveModel = vi.fn((ref) => { + if (ref?.provider === "deepseek") { + throw new Error("model missing"); + } + return { id: ref?.id || "gpt-4", provider: ref?.provider || "openai" }; + }); + + await agent.init(() => {}, { + utility_large: { id: "deepseek-v4-pro", provider: "deepseek" }, + }, resolveModel); + + expect(agent.memoryModel).toEqual({ id: "gpt-4", provider: "openai" }); + expect(agent.memoryModelUnavailableReason).toBeNull(); + expect(resolveModel).toHaveBeenCalledWith({ id: "deepseek-v4-pro", provider: "deepseek" }, expect.any(Object)); + expect(resolveModel).toHaveBeenCalledWith({ id: "gpt-4", provider: "openai" }, expect.any(Object)); + + await agent.dispose(); + }); + it("session 路径仍能用 buildSystemPrompt({ forceMemoryEnabled }) 自己构建带/不带记忆的快照", async () => { const agent = makeAgent(agentsDir, tmpDir); await agent.init(() => {}); diff --git a/tests/auto-updater.test.ts b/tests/auto-updater.test.ts index b529f598e..136a06301 100644 --- a/tests/auto-updater.test.ts +++ b/tests/auto-updater.test.ts @@ -15,7 +15,7 @@ const mockAutoUpdater = { }; const mockWindows = []; -let mockExePath = "/Applications/HanaAgent.app/Contents/MacOS/HanaAgent"; +let mockExePath = "/Applications/Xiaoan.app/Contents/MacOS/Xiaoan"; vi.mock("electron", () => ({ ipcMain: { handle: vi.fn() }, @@ -56,7 +56,7 @@ describe("auto-updater", () => { mockAutoUpdater.autoInstallOnAppQuit = true; mockAutoUpdater.allowPrerelease = false; mockAutoUpdater.installDirectory = undefined; - mockExePath = "/Applications/HanaAgent.app/Contents/MacOS/HanaAgent"; + mockExePath = "/Applications/Xiaoan.app/Contents/MacOS/Xiaoan"; ({ ipcMain } = await import("electron")); ipcMain.handle.mockImplementation((name, handler) => { @@ -99,12 +99,12 @@ describe("auto-updater", () => { try { Object.defineProperty(process, "platform", { value: "win32" }); vi.resetModules(); - mockExePath = "/tmp/HanaAgent/HanaAgent.exe"; + mockExePath = "/tmp/Xiaoan/Xiaoan.exe"; mod = await import("../desktop/auto-updater.cjs"); initWithMockWindow(); - expect(mockAutoUpdater.installDirectory).toBe("/tmp/HanaAgent"); + expect(mockAutoUpdater.installDirectory).toBe("/tmp/Xiaoan"); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } @@ -279,7 +279,7 @@ describe("auto-updater", () => { try { Object.defineProperty(process, "platform", { value: "win32" }); vi.resetModules(); - mockExePath = "/tmp/HanaAgent/HanaAgent.exe"; + mockExePath = "/tmp/Xiaoan/Xiaoan.exe"; mod = await import("../desktop/auto-updater.cjs"); initWithMockWindow(); diff --git a/tests/builtin-plugins-defaults.test.ts b/tests/builtin-plugins-defaults.test.ts new file mode 100644 index 000000000..b2d64b334 --- /dev/null +++ b/tests/builtin-plugins-defaults.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_PLUGIN_UI_TAB_ORDER } from "../shared/builtin-plugins.ts"; + +describe("builtin-plugins defaults", () => { + it("prioritizes glance next to channels in default tab order", () => { + expect(DEFAULT_PLUGIN_UI_TAB_ORDER).toEqual(["channels", "plugin:glance"]); + }); +}); + +describe("preferences plugin UI defaults", () => { + it("falls back to default tab order when plugin_ui.tabOrder is empty", async () => { + const { PreferencesManager } = await import("../core/preferences-manager.ts"); + const pm = new PreferencesManager({ + userDir: "/tmp/unused", + agentsDir: "/tmp/unused-agents", + } as any); + pm._cache = {}; + expect(pm.getPluginUiPrefs().tabOrder).toEqual([...DEFAULT_PLUGIN_UI_TAB_ORDER]); + pm._cache = { plugin_ui: { tabOrder: ["plugin:media"] } }; + expect(pm.getPluginUiPrefs().tabOrder).toEqual(["plugin:media"]); + }); +}); diff --git a/tests/channel-router-reply-tools.test.ts b/tests/channel-router-reply-tools.test.ts index bf5669890..d65e2bfe4 100644 --- a/tests/channel-router-reply-tools.test.ts +++ b/tests/channel-router-reply-tools.test.ts @@ -101,7 +101,7 @@ describe("ChannelRouter reply tool boundary", () => { const phonePrompt = rounds![0].text; expect(phonePrompt).not.toContain(""); expect(phonePrompt).not.toContain(""); - expect(phonePrompt).toContain("PULSE"); + expect(phonePrompt).toContain("心绪"); expect(phonePrompt).toContain(""); expect(phonePrompt).toContain("实际发到群聊的回复正文"); expect(phonePrompt).toContain("优先口语化"); diff --git a/tests/character-card-export-skills.test.ts b/tests/character-card-export-skills.test.ts new file mode 100644 index 000000000..c367c54f7 --- /dev/null +++ b/tests/character-card-export-skills.test.ts @@ -0,0 +1,44 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { describe, expect, it, vi } from "vitest"; +import { extractZip } from "../lib/extract-zip.ts"; +import { createCharacterCardService } from "../lib/character-cards/service.ts"; + +function writeSkill(root, name) { + const dir = path.join(root, name); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, "SKILL.md"), `---\nname: ${name}\n---\nbody`, "utf-8"); + return dir; +} + +describe("character-card export with skills", () => { + it("exports a zip when skills are enabled", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cc-export-skills-")); + const agentsDir = path.join(tempDir, "agents"); + const skillsDir = path.join(tempDir, "skills"); + fs.mkdirSync(agentsDir, { recursive: true }); + const agentDir = path.join(agentsDir, "hana"); + fs.mkdirSync(path.join(agentDir, "memory"), { recursive: true }); + fs.writeFileSync(path.join(agentDir, "config.yaml"), "agent:\n name: Hana\n yuan: hanako\nskills:\n enabled:\n - writer\n", "utf-8"); + fs.writeFileSync(path.join(agentDir, "identity.md"), "id", "utf-8"); + const skillDir = writeSkill(skillsDir, "writer"); + const engine = { + hanakoHome: tempDir, + agentsDir, + userSkillsDir: skillsDir, + skillsDir, + cwd: tempDir, + getAgent: (id) => id === "hana" ? { id: "hana", agentDir, factStore: { exportAll: () => [] } } : null, + getAllSkills: () => [{ name: "writer", enabled: true, baseDir: skillDir, filePath: path.join(skillDir, "SKILL.md"), source: "user" }], + }; + const service = createCharacterCardService(engine); + const exported = await service.exportAgentPackage("hana", { exportMemory: false, targetDir: tempDir }); + expect(fs.existsSync(exported.filePath)).toBe(true); + const outDir = path.join(tempDir, "out"); + fs.mkdirSync(outDir); + await extractZip(exported.filePath, outDir); + expect(fs.existsSync(path.join(outDir, "skills", "writer", "SKILL.md"))).toBe(true); + fs.rmSync(tempDir, { recursive: true, force: true }); + }); +}); diff --git a/tests/character-card-import.test.ts b/tests/character-card-import.test.ts index 8239bf433..112b83274 100644 --- a/tests/character-card-import.test.ts +++ b/tests/character-card-import.test.ts @@ -409,13 +409,13 @@ describe("character-card import service", () => { exportMemory: true, targetDir: tempDir, }); - expect(exported.filePath).toBe(path.join(tempDir, "hana-charactercard.zip")); + expect(exported.filePath).toBe(path.join(tempDir, "exports", "hana-charactercard.zip")); expect(fs.existsSync(exported.filePath)).toBe(true); const secondExport = await service.exportAgentPackage("hana", { exportMemory: false, targetDir: tempDir, }); - expect(secondExport.filePath).toBe(path.join(tempDir, "hana-charactercard-2.zip")); + expect(secondExport.filePath).toBe(path.join(tempDir, "exports", "hana-charactercard-2.zip")); const outDir = path.join(tempDir, "unzipped-export"); fs.mkdirSync(outDir); @@ -540,6 +540,7 @@ describe("character-card import service", () => { exportMemory: false, targetDir: tempDir, }); + expect(exported.filePath).toBe(path.join(tempDir, "exports", "quiet-charactercard.zip")); const outDir = path.join(tempDir, "unzipped-export-empty-skills"); fs.mkdirSync(outDir); await extractZip(exported.filePath, outDir); @@ -585,7 +586,7 @@ describe("character-card import service", () => { }); const exportData = await exportRes.json(); expect(exportRes.status).toBe(200); - expect(exportData.filePath).toBe(path.join(tempDir, "hana-charactercard.zip")); + expect(exportData.filePath).toBe(path.join(tempDir, "exports", "hana-charactercard.zip")); const outDir = path.join(tempDir, "unzipped-route-export"); fs.mkdirSync(outDir); diff --git a/tests/cli-local-server.test.ts b/tests/cli-local-server.test.ts index a87da0ed3..7c03dc2a7 100644 --- a/tests/cli-local-server.test.ts +++ b/tests/cli-local-server.test.ts @@ -1,56 +1,20 @@ -import { afterEach, describe, expect, it } from "vitest"; -import fs from "fs"; import os from "os"; import path from "path"; -import { readLocalServerInfo, resolveConnection } from "../cli/local-server.ts"; +import { describe, expect, it } from "vitest"; -function makeTmpDir() { - return fs.mkdtempSync(path.join(os.tmpdir(), "hana-cli-server-")); -} +import { resolveCliHanaHome } from "../cli/local-server.ts"; -describe("CLI local server discovery", () => { - let tmpDir = null; - - afterEach(() => { - if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); - tmpDir = null; - }); - - it("reports a missing server-info file explicitly", () => { - tmpDir = makeTmpDir(); - const result = readLocalServerInfo({ hanaHome: tmpDir }); - - expect(result).toMatchObject({ - ok: false, - reason: "missing_server_info", - }); - }); - - it("builds a loopback connection from server-info", () => { - tmpDir = makeTmpDir(); - fs.writeFileSync(path.join(tmpDir, "server-info.json"), JSON.stringify({ - pid: process.pid, - port: 14500, - token: "hana_token", - version: "1.2.3", - }), "utf-8"); - - expect(resolveConnection({ hanaHome: tmpDir })).toMatchObject({ - ok: true, - baseUrl: "http://127.0.0.1:14500", - token: "hana_token", - source: "server-info", - queryTokenAllowed: true, - }); +describe("cli local server discovery", () => { + it("uses Xiaoan home by default", () => { + expect(resolveCliHanaHome({})).toBe(path.join(os.homedir(), ".xiaoan")); }); - it("uses explicit URL without allowing query-token transport by default", () => { - expect(resolveConnection({ url: "http://example.com/", token: "device" })).toMatchObject({ - ok: true, - baseUrl: "http://example.com", - token: "device", - source: "explicit", - queryTokenAllowed: false, - }); + it("prefers XIAOAN_HOME and keeps legacy HANA_HOME compatibility", () => { + expect(resolveCliHanaHome({ XIAOAN_HOME: "~/.xiaoan-dev", HANA_HOME: "~/.hanako-dev" })).toBe( + path.join(os.homedir(), ".xiaoan-dev"), + ); + expect(resolveCliHanaHome({ HANA_HOME: "~/.hanako-dev" })).toBe( + path.join(os.homedir(), ".hanako-dev"), + ); }); }); diff --git a/tests/config-loader.test.ts b/tests/config-loader.test.ts index 4db980224..cdc4d8207 100644 --- a/tests/config-loader.test.ts +++ b/tests/config-loader.test.ts @@ -102,6 +102,13 @@ describe("loadConfig", () => { }); describe("saveConfig", () => { + it("writes xiaoan-branded yaml header on save", () => { + writeYaml({ api: { provider: "openai", api_key: "sk-1", base_url: "https://api.openai.com/v1" } }); + saveConfig(configPath, { user: { name: "Test" } }); + const raw = fs.readFileSync(configPath, "utf-8"); + expect(raw.startsWith("# 小安 系统配置\n")).toBe(true); + }); + it("deep merge 保留已有字段", () => { writeYaml({ api: { provider: "openai", api_key: "sk-1", base_url: "https://api.openai.com/v1" }, user: { name: "Alice" } }); saveConfig(configPath, { user: { age: 18 } }); diff --git a/tests/config-workspaces-route.test.ts b/tests/config-workspaces-route.test.ts index 1520ebdde..2bdf3dd11 100644 --- a/tests/config-workspaces-route.test.ts +++ b/tests/config-workspaces-route.test.ts @@ -138,7 +138,7 @@ describe("config workspace routes", () => { const app = new Hono(); app.route("/api", createConfigRoute(engine)); - const expected = path.join(homeDir, "Desktop", "OH-WorkSpace"); + const expected = path.join(homeDir, "Desktop", "Xiaoan-Workspace"); const getRes = await app.request("/api/config/default-workspace"); expect(getRes.status).toBe(200); diff --git a/tests/default-workspace.test.ts b/tests/default-workspace.test.ts index b27b1f739..e7d4dead4 100644 --- a/tests/default-workspace.test.ts +++ b/tests/default-workspace.test.ts @@ -13,9 +13,9 @@ describe("default workspace contract", () => { it("places the default workspace under the user's Desktop", () => { const homeDir = path.join(os.tmpdir(), "hana-default-workspace-home"); - expect(DEFAULT_WORKSPACE_DIRNAME).toBe("OH-WorkSpace"); + expect(DEFAULT_WORKSPACE_DIRNAME).toBe("Xiaoan-Workspace"); expect(resolveDefaultWorkspacePath(homeDir)).toBe( - path.join(homeDir, "Desktop", "OH-WorkSpace"), + path.join(homeDir, "Desktop", "Xiaoan-Workspace"), ); }); @@ -31,7 +31,7 @@ describe("default workspace contract", () => { const coordinator = new ConfigCoordinator({ getAgentById: () => ({ config: { desk: {} } }), }); - const expected = path.join(tmpHome, "Desktop", "OH-WorkSpace"); + const expected = path.join(tmpHome, "Desktop", "Xiaoan-Workspace"); expect(coordinator.getHomeFolder("hana")).toBe(expected); expect(fs.existsSync(expected)).toBe(false); diff --git a/tests/deferred-result-interlude.test.ts b/tests/deferred-result-interlude.test.ts index f7ad10b2a..4a9bbe2ab 100644 --- a/tests/deferred-result-interlude.test.ts +++ b/tests/deferred-result-interlude.test.ts @@ -17,14 +17,14 @@ describe("deferred result interlude", () => { label: "大纲评估", summary: "请阅读整份长任务说明并输出完整评估", }, - }, { receiverName: "小花" }); + }, { receiverName: "小安" }); expect(block).toMatchObject({ type: "interlude", taskId: "subagent-1", sourceKind: "subagent", sourceLabel: "明 · 大纲评估", - text: "小花 收到了来自 明 · 大纲评估 的回复", + text: "小安 收到了来自 明 · 大纲评估 的回复", detailMarkdown: "整理完成", }); }); @@ -40,12 +40,12 @@ describe("deferred result interlude", () => { label: "凌晨诗行", summary: "写一首关于凌晨五点三十九分的三行短诗。要求:不要使用常见意象。", }, - }, { receiverName: "Hanako" }); + }, { receiverName: "小安" }); expect(block).toMatchObject({ sourceKind: "subagent", sourceLabel: "Hanako · 凌晨诗行", - text: "Hanako 收到了来自 Hanako · 凌晨诗行 的回复", + text: "小安 收到了来自 Hanako · 凌晨诗行 的回复", }); expect(block.sourceLabel).not.toContain("写一首"); expect(block.text).not.toContain("五点三十九分"); diff --git a/tests/desktop-launch-bootstrap-contract.test.ts b/tests/desktop-launch-bootstrap-contract.test.ts index 90d9c1857..ab6d2868a 100644 --- a/tests/desktop-launch-bootstrap-contract.test.ts +++ b/tests/desktop-launch-bootstrap-contract.test.ts @@ -5,6 +5,7 @@ import path from "path"; const root = process.cwd(); const packageJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8")); const bootstrapPath = path.join(root, "desktop", "bootstrap.cjs"); +const mainPath = path.join(root, "desktop", "main.cjs"); describe("desktop launch bootstrap contract", () => { it("routes dev and packaged Electron through the pre-main bootstrap", () => { @@ -35,5 +36,15 @@ describe("desktop launch bootstrap contract", () => { expect(markerIndex).toBeLessThan(loadIndex); expect(source).toContain("main.bundle.cjs"); expect(source).toContain("main.cjs"); + expect(source).toContain("EIO"); + }); + + it("does not recursively log closed process output errors", () => { + const source = fs.readFileSync(mainPath, "utf-8"); + + expect(source).toContain("function isProcessOutputClosedError"); + expect(source).toContain("safeConsoleError"); + expect(source).toContain('"EIO"'); + expect(source).toContain('"ERR_STREAM_DESTROYED"'); }); }); diff --git a/tests/desktop-single-instance-lock.test.ts b/tests/desktop-single-instance-lock.test.ts index 02d43aab6..3b6559cdd 100644 --- a/tests/desktop-single-instance-lock.test.ts +++ b/tests/desktop-single-instance-lock.test.ts @@ -46,7 +46,7 @@ describe("desktop client single instance lock", () => { ); }); - it("keeps production in the legacy Electron userData namespace", () => { + it("keeps production in the canonical Xiaoan Electron userData namespace", () => { const { app } = makeApp(); const defaultHome = path.join("C:", "Users", "me", ".hanako"); @@ -59,7 +59,7 @@ describe("desktop client single instance lock", () => { expect(acquired).toBe(true); expect(app.setPath).toHaveBeenCalledWith( "userData", - path.join("C:", "Users", "me", "AppData", "Roaming", "Hanako"), + path.join("C:", "Users", "me", "AppData", "Roaming", "Xiaoan"), ); expect(app.requestSingleInstanceLock).toHaveBeenCalledTimes(1); }); diff --git a/tests/extract-zip-symlink-defense.test.ts b/tests/extract-zip-symlink-defense.test.ts index 8470188a5..cb2f81bf4 100644 --- a/tests/extract-zip-symlink-defense.test.ts +++ b/tests/extract-zip-symlink-defense.test.ts @@ -2,23 +2,22 @@ import fs from "fs"; import os from "os"; import path from "path"; import { describe, expect, it } from "vitest"; -import archiver from "archiver"; +import { strToU8, zipSync, type Zippable } from "fflate"; import { extractZip } from "../lib/extract-zip.ts"; -function buildZipWithSymlink(zipPath: any, { symlinkName, symlinkTarget, fileEntries = [] }: any) { - return new Promise((resolve, reject) => { - const output = fs.createWriteStream(zipPath); - const archive = archiver("zip", { zlib: { level: 1 } }); - output.on("close", resolve); - archive.on("error", reject); - archive.pipe(output); - if (symlinkName) archive.symlink(symlinkName, symlinkTarget); - for (const entry of fileEntries) { - archive.append(entry.content, { name: entry.name }); - } - archive.finalize(); - }); +function buildZipWithSymlink(zipPath: string, { symlinkName, symlinkTarget, fileEntries = [] }: any) { + const entries: Zippable = {}; + for (const entry of fileEntries) { + entries[entry.name] = strToU8(entry.content); + } + if (symlinkName) { + entries[symlinkName] = [ + strToU8(symlinkTarget), + { os: 3, attrs: 0o120777 << 16, level: 0 }, + ]; + } + fs.writeFileSync(zipPath, zipSync(entries, { level: 0 })); } describe("lib/extract-zip", () => { diff --git a/tests/first-run-default-workspace.test.ts b/tests/first-run-default-workspace.test.ts index d09ca3d01..dff697f0a 100644 --- a/tests/first-run-default-workspace.test.ts +++ b/tests/first-run-default-workspace.test.ts @@ -4,6 +4,7 @@ import path from "path"; import YAML from "js-yaml"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { CANONICAL_AGENT_ID, LEGACY_AGENT_ID } from "../shared/xiaoan-runtime-contract.ts"; +import { XIAOAN_BUILTIN_RELAY_PROVIDER_ID } from "../shared/xiaoan-builtin-relay.ts"; describe("first run default workspace", () => { let tmpDir; @@ -55,12 +56,51 @@ describe("first run default workspace", () => { expect(cfg.desk.heartbeat_enabled).toBe(false); expect(cfg.desk.heartbeat_interval).toBe(31); expect(cfg.memory.enabled).toBe(true); - expect(cfg.agent.name).toBe("小小安"); + expect(cfg.agent.name).toBe("小安"); expect(cfg.agent.yuan).toBe("xiaoan"); expect(cfg.skills.enabled).toContain("xiaoan-handbook"); }); - it("keeps userName as a dynamic identity placeholder for first-run Minian", async () => { + it("seeds the builtin Xiaoan relay provider without writing credentials", async () => { + const { ensureFirstRun } = await import("../core/first-run.ts"); + + ensureFirstRun(hanakoHome, productDir); + + const raw = YAML.load(fs.readFileSync(path.join(hanakoHome, "added-models.yaml"), "utf-8")); + expect(raw.providers[XIAOAN_BUILTIN_RELAY_PROVIDER_ID]).toEqual({ + base_url: "https://an520.xin/v1", + api: "openai-completions", + }); + expect(raw.providers[XIAOAN_BUILTIN_RELAY_PROVIDER_ID]).not.toHaveProperty("api_key"); + }); + + it("does not overwrite an existing builtin relay provider", async () => { + fs.mkdirSync(hanakoHome, { recursive: true }); + fs.writeFileSync( + path.join(hanakoHome, "added-models.yaml"), + YAML.dump({ + providers: { + [XIAOAN_BUILTIN_RELAY_PROVIDER_ID]: { + base_url: "https://relay.example/v1", + api: "openai-completions", + models: ["existing-model"], + }, + }, + }), + "utf-8", + ); + const { ensureFirstRun } = await import("../core/first-run.ts"); + + ensureFirstRun(hanakoHome, productDir); + + const raw = YAML.load(fs.readFileSync(path.join(hanakoHome, "added-models.yaml"), "utf-8")); + expect(raw.providers[XIAOAN_BUILTIN_RELAY_PROVIDER_ID]).toMatchObject({ + base_url: "https://relay.example/v1", + models: ["existing-model"], + }); + }); + + it("keeps userName as a dynamic identity placeholder for first-run Xiaoan", async () => { fs.mkdirSync(path.join(productDir, "identity-templates"), { recursive: true }); fs.writeFileSync( path.join(productDir, "identity-templates", `${LEGACY_AGENT_ID}.md`), @@ -88,7 +128,7 @@ describe("first run default workspace", () => { const cfgPath = path.join(hanakoHome, "agents", CANONICAL_AGENT_ID, "config.yaml"); const cfg = YAML.load(fs.readFileSync(cfgPath, "utf-8")); - expect(cfg.agent.name).toBe("小小安"); + expect(cfg.agent.name).toBe("小安"); expect(fs.statSync(path.join(hanakoHome, "agents", CANONICAL_AGENT_ID, "sessions")).isDirectory()).toBe(true); }); @@ -132,7 +172,7 @@ describe("first run default workspace", () => { const minianDir = path.join(hanakoHome, "agents", CANONICAL_AGENT_ID); const cfg = YAML.load(fs.readFileSync(path.join(minianDir, "config.yaml"), "utf-8")); - expect(cfg.agent.name).toBe("小小安"); + expect(cfg.agent.name).toBe("小安"); expect(report.repairedDefaultAgent).toBe(true); const backups = fs.readdirSync(minianDir).filter((name) => name.startsWith("config.yaml.broken-")); expect(backups).toHaveLength(1); diff --git a/tests/glance-config.test.ts b/tests/glance-config.test.ts new file mode 100644 index 000000000..f3c866155 --- /dev/null +++ b/tests/glance-config.test.ts @@ -0,0 +1,39 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + listGlanceWidgets, + readGlanceConfig, + updateMonitorSites, + writeGlanceConfig, +} from "../plugins/glance/lib/glance-config.ts"; + +describe("glance-config", () => { + let tmpDir = ""; + + afterEach(() => { + if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); + tmpDir = ""; + }); + + it("creates default glance.yml on first read", () => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "glance-config-")); + const { yaml, parsed } = readGlanceConfig(tmpDir); + expect(yaml).toContain("type: monitor"); + expect(listGlanceWidgets(parsed).some((w) => w.type === "monitor")).toBe(true); + expect(fs.existsSync(path.join(tmpDir, "glance.yml"))).toBe(true); + }); + + it("updates monitor sites in place", () => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "glance-config-")); + const { parsed } = readGlanceConfig(tmpDir); + const next = updateMonitorSites(parsed, { + title: "Services", + sites: [{ title: "API", url: "https://example.com" }], + }); + const saved = writeGlanceConfig(tmpDir, `pages:\n - name: Home\n columns:\n - size: full\n widgets:\n - type: monitor\n title: Services\n sites:\n - title: API\n url: https://example.com\n`); + expect(saved.yaml).toContain("https://example.com"); + expect(listGlanceWidgets(next).length).toBeGreaterThan(0); + }); +}); diff --git a/tests/launch-integrity.test.ts b/tests/launch-integrity.test.ts index d8c4a99e3..99aee4eda 100644 --- a/tests/launch-integrity.test.ts +++ b/tests/launch-integrity.test.ts @@ -34,6 +34,15 @@ afterEach(() => { }); describe("desktop launch integrity helper", () => { + it("defaults to Xiaoan.exe label when execPath is missing", () => { + const helper = loadHelper(); + if (!helper) return; + + const result = helper.checkWindowsInstallSurface({}); + expect(result.missing[0].relativePath).toBe("Xiaoan.exe"); + expect(helper.formatInstallSurfaceError(result)).toContain("Xiaoan installation is incomplete."); + }); + it("covers the Windows install surface needed before desktop main loads", () => { const helper = loadHelper(); if (!helper) return; @@ -60,7 +69,7 @@ describe("desktop launch integrity helper", () => { "HanaAgent.exe", "resources/app.asar", "resources/app-update.yml", - "resources/server/hana-server.exe", + "resources/server/xiaoan-server.exe", "resources/server/bootstrap.js", "resources/server/bundle/index.js", "resources/server/node_modules/better-sqlite3/build/Release/better_sqlite3.node", @@ -77,7 +86,7 @@ describe("desktop launch integrity helper", () => { writeFile(tmp, "HanaAgent.exe"); writeFile(resourcesPath, "app.asar"); writeFile(resourcesPath, "app-update.yml"); - writeFile(resourcesPath, "server/hana-server.exe"); + writeFile(resourcesPath, "server/xiaoan-server.exe"); writeFile(resourcesPath, "server/bootstrap.js"); writeFile(resourcesPath, "server/bundle/index.js"); writeFile(resourcesPath, "server/node_modules/better-sqlite3/build/Release/better_sqlite3.node"); @@ -112,7 +121,7 @@ describe("desktop launch integrity helper", () => { writeFile(resourcesPath, "app/desktop/bootstrap.cjs"); writeFile(resourcesPath, "app/package.json"); writeFile(resourcesPath, "app-update.yml"); - writeFile(resourcesPath, "server/hana-server.exe"); + writeFile(resourcesPath, "server/xiaoan-server.exe"); writeFile(resourcesPath, "server/bootstrap.js"); writeFile(resourcesPath, "server/bundle/index.js"); writeFile(resourcesPath, "server/node_modules/better-sqlite3/build/Release/better_sqlite3.node"); diff --git a/tests/llm-client-provider-compat.test.ts b/tests/llm-client-provider-compat.test.ts index 432545cca..4144583b2 100644 --- a/tests/llm-client-provider-compat.test.ts +++ b/tests/llm-client-provider-compat.test.ts @@ -512,7 +512,7 @@ describe("callText provider-compat routing", () => { timeoutMs: 5_000, } as any); - expect(fetchMock.mock.calls[0][1].headers["User-Agent"]).toBe("HanaAgent/1.0"); + expect(fetchMock.mock.calls[0][1].headers["User-Agent"]).toBe("Xiaoan/1.0"); expect(fetchMock.mock.calls[1][1].headers["User-Agent"]).toBe("ExistingClient/2.0"); }); diff --git a/tests/locale-integrity.test.ts b/tests/locale-integrity.test.ts new file mode 100644 index 000000000..caacdd04e --- /dev/null +++ b/tests/locale-integrity.test.ts @@ -0,0 +1,102 @@ +import fs from 'fs'; +import path from 'path'; +import { describe, expect, it } from 'vitest'; + +const ROOT = path.resolve(process.cwd()); +const LOCALES_DIR = path.join(ROOT, 'desktop/src/locales'); +const LOCALE_FILES = fs.readdirSync(LOCALES_DIR).filter((name) => name.endsWith('.json')); + +function resolveKey(obj: unknown, dottedKey: string): unknown { + return dottedKey.split('.').reduce((acc, key) => { + if (acc && typeof acc === 'object' && key in (acc as Record)) { + return (acc as Record)[key]; + } + return undefined; + }, obj); +} + +function expectNonEmptyString(value: unknown, label: string) { + expect(typeof value, label).toBe('string'); + expect(String(value).trim().length, label).toBeGreaterThan(0); +} + +function expectNonEmptyStringArray(value: unknown, label: string) { + expect(Array.isArray(value), label).toBe(true); + expect((value as unknown[]).length, label).toBeGreaterThan(0); + for (const [index, item] of (value as unknown[]).entries()) { + expectNonEmptyString(item, `${label}[${index}]`); + } +} + +function collectStrings(value: unknown, pathParts: string[] = [], out: Array<{ path: string; value: string }> = []) { + if (typeof value === 'string') { + out.push({ path: pathParts.join('.'), value }); + return out; + } + if (Array.isArray(value)) { + value.forEach((item, index) => collectStrings(item, [...pathParts, String(index)], out)); + return out; + } + if (value && typeof value === 'object') { + Object.entries(value).forEach(([key, item]) => collectStrings(item, [...pathParts, key], out)); + } + return out; +} + +describe('locale integrity', () => { + it('all locale JSON files parse', () => { + for (const file of LOCALE_FILES) { + const raw = fs.readFileSync(path.join(LOCALES_DIR, file), 'utf8'); + expect(() => JSON.parse(raw), `${file} must be valid JSON`).not.toThrow(); + } + }); + + describe.each(LOCALE_FILES)('%s yuan personality contract', (file) => { + const data = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, file), 'utf8')); + + it('defines butter and ming type metadata', () => { + for (const yuan of ['butter', 'ming'] as const) { + expectNonEmptyString(resolveKey(data, `yuan.types.${yuan}.name`), `${file} yuan.types.${yuan}.name`); + expectNonEmptyString(resolveKey(data, `yuan.types.${yuan}.label`), `${file} yuan.types.${yuan}.label`); + expectNonEmptyString(resolveKey(data, `yuan.types.${yuan}.avatar`), `${file} yuan.types.${yuan}.avatar`); + } + }); + + it('defines splash, welcome, placeholder, and thinking copy for butter and ming', () => { + for (const yuan of ['butter', 'ming'] as const) { + expectNonEmptyStringArray(resolveKey(data, `yuan.splash.${yuan}`), `${file} yuan.splash.${yuan}`); + expectNonEmptyStringArray(resolveKey(data, `yuan.welcome.${yuan}`), `${file} yuan.welcome.${yuan}`); + expectNonEmptyString(resolveKey(data, `yuan.placeholder.${yuan}`), `${file} yuan.placeholder.${yuan}`); + expectNonEmptyStringArray(resolveKey(data, `thinking.${yuan}`), `${file} thinking.${yuan}`); + } + }); + }); + + describe.each(LOCALE_FILES)('%s Xiaoan product copy contract', (file) => { + const data = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, file), 'utf8')); + + it('defines onboarding mode and built-in relay copy', () => { + for (const key of [ + 'onboarding.provider.title', + 'onboarding.provider.subtitle', + 'onboarding.tutorial.assistants.title', + 'onboarding.tutorial.assistants.desc', + 'settings.providers.builtinRelay.title', + 'settings.providers.builtinRelay.description', + 'settings.providers.builtinRelay.integrationNote', + 'settings.providers.builtinRelay.knownIssuesTitle', + ]) { + expectNonEmptyString(resolveKey(data, key), `${file} ${key}`); + } + }); + + it('does not expose upstream framework terms in visible copy', () => { + const upstreamCopy = /OpenHanako|Hanako|HanaAgent|\bHana\b|角色卡|笺|\bAgent\b|Bridge|Plugin|\bSkills?\b|Loading\.\.\.|Error 404|Error:/; + const hits = collectStrings(data) + .filter(({ value }) => upstreamCopy.test(value)) + .map(({ path: keyPath, value }) => `${keyPath}: ${value}`); + + expect(hits, `${file} contains upstream/framework visible copy`).toEqual([]); + }); + }); +}); diff --git a/tests/mac-update-metadata-contract.test.ts b/tests/mac-update-metadata-contract.test.ts index 53dfc1ad4..e8c89e046 100644 --- a/tests/mac-update-metadata-contract.test.ts +++ b/tests/mac-update-metadata-contract.test.ts @@ -12,7 +12,7 @@ const workflowPath = path.join(rootDir, ".github", "workflows", "build.yml"); let tmpDir; function makeTmpDir() { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "hana-mac-update-metadata-")); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "xiaoan-mac-update-metadata-")); return tmpDir; } @@ -26,17 +26,17 @@ function writeMacMetadata(artifactsDir, arch, { fileName = "latest-mac.yml" } = version, files: [ { - url: `HanaAgent-${version}-macOS-${arch}.zip`, + url: `Xiaoan-${version}-macOS-${arch}.zip`, sha512: `${arch}-zip-sha512`, size: arch === "arm64" ? 265_000_001 : 273_000_001, }, { - url: `HanaAgent-${version}-macOS-${arch}.dmg`, + url: `Xiaoan-${version}-macOS-${arch}.dmg`, sha512: `${arch}-dmg-sha512`, size: arch === "arm64" ? 271_000_001 : 280_000_001, }, ], - path: `HanaAgent-${version}-macOS-${arch}.zip`, + path: `Xiaoan-${version}-macOS-${arch}.zip`, sha512: `${arch}-zip-sha512`, releaseDate: "2026-05-09T12:52:00.000Z", }), @@ -77,12 +77,12 @@ describe("macOS update metadata release contract", () => { const urls = merged.files.map((file) => file.url); expect(urls).toEqual([ - "HanaAgent-0.171.5-macOS-arm64.zip", - "HanaAgent-0.171.5-macOS-arm64.dmg", - "HanaAgent-0.171.5-macOS-x64.zip", - "HanaAgent-0.171.5-macOS-x64.dmg", + "Xiaoan-0.171.5-macOS-arm64.zip", + "Xiaoan-0.171.5-macOS-arm64.dmg", + "Xiaoan-0.171.5-macOS-x64.zip", + "Xiaoan-0.171.5-macOS-x64.dmg", ]); - expect(merged.path).toBe("HanaAgent-0.171.5-macOS-arm64.zip"); + expect(merged.path).toBe("Xiaoan-0.171.5-macOS-arm64.zip"); expect(merged.sha512).toBe("arm64-zip-sha512"); }); @@ -121,7 +121,7 @@ describe("macOS update metadata release contract", () => { const merged = mergeLatestMacWithoutYamlDependency(artifactsDir, outputPath, hookPath); expect(merged.files).toHaveLength(4); - expect(merged.path).toBe("HanaAgent-0.171.5-macOS-arm64.zip"); + expect(merged.path).toBe("Xiaoan-0.171.5-macOS-arm64.zip"); }); it("routes the GitHub release workflow through the checked merge script", () => { diff --git a/tests/migrations.test.ts b/tests/migrations.test.ts index c78ed5f89..91fd780f3 100644 --- a/tests/migrations.test.ts +++ b/tests/migrations.test.ts @@ -14,6 +14,56 @@ import { SEARCH_CAPABILITY_PROVIDERS } from "../shared/search-providers.ts"; const LATEST_DATA_VERSION = 43; +const PI_PROVIDER_ENV_KEYS = [ + "ANTHROPIC_OAUTH_TOKEN", + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "AZURE_OPENAI_API_KEY", + "DEEPSEEK_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_CLOUD_API_KEY", + "GOOGLE_APPLICATION_CREDENTIALS", + "GOOGLE_CLOUD_PROJECT", + "GCLOUD_PROJECT", + "GOOGLE_CLOUD_LOCATION", + "GROQ_API_KEY", + "CEREBRAS_API_KEY", + "XAI_API_KEY", + "OPENROUTER_API_KEY", + "AI_GATEWAY_API_KEY", + "ZAI_API_KEY", + "MISTRAL_API_KEY", + "MINIMAX_API_KEY", + "MINIMAX_CN_API_KEY", + "HF_TOKEN", + "FIREWORKS_API_KEY", + "OPENCODE_API_KEY", + "KIMI_API_KEY", + "COPILOT_GITHUB_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", + "AWS_PROFILE", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_BEARER_TOKEN_BEDROCK", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "AWS_WEB_IDENTITY_TOKEN_FILE", +]; + +async function withPiProviderEnvCleared(run) { + const previous = new Map(PI_PROVIDER_ENV_KEYS.map((key) => [key, process.env[key]])); + for (const key of PI_PROVIDER_ENV_KEYS) delete process.env[key]; + try { + return await run(); + } finally { + for (const [key, value] of previous) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } +} + function makeTmpDir() { return fs.mkdtempSync(path.join(os.tmpdir(), "hana-migrations-")); } @@ -2271,7 +2321,7 @@ describe("migration #20: Pi model input schema compatibility", () => { const { AuthStorage, createModelRegistry } = await import("../lib/pi-sdk/index.ts"); const registry = createModelRegistry(new (AuthStorage as any)(tmpDir), modelsJsonPath); - const available = await registry.getAvailable(); + const available = await withPiProviderEnvCleared(() => registry.getAvailable()); expect(available.map((model) => model.id)).toEqual(["qwen3-vl-plus", "qwen-plus", "custom-video"]); expect(prefs.getPreferences()._dataVersion).toBe(LATEST_DATA_VERSION); }); @@ -2882,7 +2932,7 @@ describe("migration #18 — create local identity registries", () => { expect(serverNode).toEqual(expect.objectContaining({ schemaVersion: 1, - label: "Local Hana", + label: "Local Xiaoan", })); expect(serverNode.serverId).toMatch(/^server_[0-9a-f-]{36}$/); diff --git a/tests/model-sync-routes.test.ts b/tests/model-sync-routes.test.ts index a4af23afa..7f133d278 100644 --- a/tests/model-sync-routes.test.ts +++ b/tests/model-sync-routes.test.ts @@ -1436,7 +1436,7 @@ describe("model sync related routes", () => { const headers = fetchMock.mock.calls[0][1].headers; expect(headers["x-api-key"]).toBe("sk-test"); expect(headers["anthropic-version"]).toBe("2023-06-01"); - expect(headers["User-Agent"]).toBe("HanaAgent/1.0"); + expect(headers["User-Agent"]).toBe("Xiaoan/1.0"); const data = await res.json(); expect(data.models).toEqual([ diff --git a/tests/model-sync.test.ts b/tests/model-sync.test.ts index a32311a74..eeb8b7e38 100644 --- a/tests/model-sync.test.ts +++ b/tests/model-sync.test.ts @@ -10,6 +10,56 @@ import os from "os"; import path from "path"; import YAML from "js-yaml"; +const PI_PROVIDER_ENV_KEYS = [ + "ANTHROPIC_OAUTH_TOKEN", + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "AZURE_OPENAI_API_KEY", + "DEEPSEEK_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_CLOUD_API_KEY", + "GOOGLE_APPLICATION_CREDENTIALS", + "GOOGLE_CLOUD_PROJECT", + "GCLOUD_PROJECT", + "GOOGLE_CLOUD_LOCATION", + "GROQ_API_KEY", + "CEREBRAS_API_KEY", + "XAI_API_KEY", + "OPENROUTER_API_KEY", + "AI_GATEWAY_API_KEY", + "ZAI_API_KEY", + "MISTRAL_API_KEY", + "MINIMAX_API_KEY", + "MINIMAX_CN_API_KEY", + "HF_TOKEN", + "FIREWORKS_API_KEY", + "OPENCODE_API_KEY", + "KIMI_API_KEY", + "COPILOT_GITHUB_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", + "AWS_PROFILE", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_BEARER_TOKEN_BEDROCK", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "AWS_WEB_IDENTITY_TOKEN_FILE", +]; + +async function withPiProviderEnvCleared(run) { + const previous = new Map(PI_PROVIDER_ENV_KEYS.map((key) => [key, process.env[key]])); + for (const key of PI_PROVIDER_ENV_KEYS) delete process.env[key]; + try { + return await run(); + } finally { + for (const [key, value] of previous) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } +} + // mock known-models 词典查询:provider + model 二级结构,未命中时再查通用 fallback const KNOWN_MODELS = { dashscope: { @@ -1153,7 +1203,7 @@ describe("syncModels", () => { syncModels(providers, { modelsJsonPath }); const registry = createModelRegistry(new (AuthStorage as any)(tmpDir), modelsJsonPath); - const available = await registry.getAvailable(); + const available = await withPiProviderEnvCleared(() => registry.getAvailable()); expect(available).toHaveLength(1); expect(available[0]).toMatchObject({ id: "qwen3-vl-plus", diff --git a/tests/motion-failsafe.test.ts b/tests/motion-failsafe.test.ts new file mode 100644 index 000000000..6c530a5e7 --- /dev/null +++ b/tests/motion-failsafe.test.ts @@ -0,0 +1,71 @@ +// @vitest-environment jsdom + +import { describe, expect, it } from 'vitest'; +import gsap from 'gsap'; +import { + ensureMotionVisible, + installMotionVisibilityFailsafe, +} from '../desktop/src/react/motion/motion-failsafe.ts'; +import { animateAppShellEnter } from '../desktop/src/react/motion/xiaoan-gsap.ts'; + +describe('motion visibility failsafe', () => { + it('ensureMotionVisible repairs stuck app-shell opacity', () => { + document.body.innerHTML = ` +
+
+ +
+
+ `; + const shell = document.querySelector('.app-shell') as HTMLElement; + const main = document.querySelector('.main-content') as HTMLElement; + + const tl = animateAppShellEnter(shell); + tl?.progress(0.02); + tl?.kill(); + main.style.opacity = '0'; + + expect(ensureMotionVisible()).toBe(true); + expect(getComputedStyle(main).opacity).toBe('1'); + gsap.globalTimeline.clear(); + }); + + it('installMotionVisibilityFailsafe is idempotent', () => { + const stopA = installMotionVisibilityFailsafe(); + const stopB = installMotionVisibilityFailsafe(); + expect(stopA).toBeTypeOf('function'); + expect(stopB).toBeTypeOf('function'); + stopA(); + stopB(); + }); + + it('installMotionVisibilityFailsafe falls back when window timers are unavailable', () => { + const originalSetTimeout = window.setTimeout; + const originalClearTimeout = window.clearTimeout; + + try { + Object.defineProperty(window, 'setTimeout', { + configurable: true, + value: undefined, + }); + Object.defineProperty(window, 'clearTimeout', { + configurable: true, + value: undefined, + }); + + const stop = installMotionVisibilityFailsafe(); + + expect(stop).toBeTypeOf('function'); + stop(); + } finally { + Object.defineProperty(window, 'setTimeout', { + configurable: true, + value: originalSetTimeout, + }); + Object.defineProperty(window, 'clearTimeout', { + configurable: true, + value: originalClearTimeout, + }); + } + }); +}); diff --git a/tests/office-plugin-tools.test.ts b/tests/office-plugin-tools.test.ts index 0e34945a3..deed029fe 100644 --- a/tests/office-plugin-tools.test.ts +++ b/tests/office-plugin-tools.test.ts @@ -288,7 +288,7 @@ describe("office plugin tools", () => { }, { env: { - HANA_DESKTOP_EXEC_PATH: "/Applications/HanaAgent.app/Contents/MacOS/HanaAgent", + HANA_DESKTOP_EXEC_PATH: "/Applications/Xiaoan.app/Contents/MacOS/Xiaoan", HANA_DESKTOP_IS_PACKAGED: "1", }, spawn: fakeSpawn, @@ -296,7 +296,7 @@ describe("office plugin tools", () => { ); expect(observedCommand).toMatchObject({ - command: "/Applications/HanaAgent.app/Contents/MacOS/HanaAgent", + command: "/Applications/Xiaoan.app/Contents/MacOS/Xiaoan", args: ["--hana-office-html-to-pdf", expect.stringMatching(/job\.json$/)], }); expect(fs.readFileSync(result.outputPath, "utf-8")).toContain("%PDF-1.4"); @@ -342,7 +342,7 @@ describe("office plugin tools", () => { }, { env: { - HANA_DESKTOP_EXEC_PATH: "/Applications/HanaAgent.app/Contents/MacOS/HanaAgent", + HANA_DESKTOP_EXEC_PATH: "/Applications/Xiaoan.app/Contents/MacOS/Xiaoan", HANA_DESKTOP_IS_PACKAGED: "1", }, spawn: fakeSpawn, diff --git a/tests/plugin-glance.test.ts b/tests/plugin-glance.test.ts new file mode 100644 index 000000000..1337e7313 --- /dev/null +++ b/tests/plugin-glance.test.ts @@ -0,0 +1,91 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { afterEach, describe, expect, it } from "vitest"; +import { PluginManager } from "../core/plugin-manager.ts"; +import registerGlancePage from "../plugins/glance/routes/page.js"; + +type TestRouteHandler = (ctx: any) => string; + +describe("glance builtin plugin", () => { + let dataDir = ""; + let builtinDir = ""; + let communityDir = ""; + + afterEach(() => { + if (dataDir) fs.rmSync(dataDir, { recursive: true, force: true }); + if (communityDir) fs.rmSync(communityDir, { recursive: true, force: true }); + dataDir = ""; + communityDir = ""; + }); + + it("loads page, tools, and skill contributions", async () => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "hana-data-")); + communityDir = fs.mkdtempSync(path.join(os.tmpdir(), "hana-community-")); + builtinDir = path.join(process.cwd(), "plugins"); + + const pm = new PluginManager({ + pluginsDirs: [builtinDir, communityDir], + dataDir, + bus: { + emit() {}, + subscribe() { return () => {}; }, + handle() { return () => {}; }, + request: async () => null, + }, + preferencesManager: { + getAllowFullAccessPlugins: () => true, + getDisabledPlugins: () => [], + }, + } as any); + + await pm.loadAll(); + const pages = pm.getPages(); + const glancePage = pages.find((p) => p.pluginId === "glance"); + expect(glancePage).toBeTruthy(); + expect(glancePage?.route).toBe("/page"); + + const tools = pm.getAllTools().map((t) => t.name); + expect(tools).toContain("glance_read-config"); + expect(tools).toContain("glance_write-config"); + expect(tools).toContain("glance_update-monitor"); + + const skillPaths = pm.getSkillPaths(); + expect(skillPaths.some((p) => String(p.dirPath || "").split(path.sep).join("/").includes("plugins/glance/skills"))).toBe(true); + }); + + it("keeps the built-in page usable inside Xiaoan's authenticated plugin iframe", () => { + const routes: Record = {}; + registerGlancePage({ + get(route: string, handler: TestRouteHandler) { + routes[route] = handler; + }, + } as any, { pluginId: "glance" }); + + const html = routes["/page"]({ + req: { + query(key: string) { + return ({ + token: "owner-token", + pluginSurfaceSession: "surface-session", + agentId: "minian", + "hana-theme": "xiaoan-console", + "hana-css": "http://127.0.0.1:14500/api/plugins/theme.css?token=owner-token", + } as Record)[key] || ""; + }, + }, + html(value: string) { + return value; + }, + }); + + expect(html).toContain("/api/plugins/glance/assets/panel.css?token=owner-token&pluginSurfaceSession=surface-session&agentId=minian"); + expect(html).toContain("/api/plugins/glance/assets/panel.js?token=owner-token&pluginSurfaceSession=surface-session&agentId=minian"); + + const panelSource = fs.readFileSync(path.join(process.cwd(), "plugins", "glance", "assets", "panel.js"), "utf-8"); + expect(panelSource).toContain("buildAuthenticatedPluginUrl"); + expect(panelSource).toContain("pluginSurfaceSession"); + expect(panelSource).toContain("小安概览"); + expect(panelSource).not.toContain("glanceapp/glance"); + }); +}); diff --git a/tests/preload-bridge-contract.test.ts b/tests/preload-bridge-contract.test.ts new file mode 100644 index 000000000..d949ecc7b --- /dev/null +++ b/tests/preload-bridge-contract.test.ts @@ -0,0 +1,15 @@ +import fs from "fs"; +import path from "path"; +import { describe, expect, it } from "vitest"; + +const root = process.cwd(); + +describe("desktop preload bridge contract", () => { + it("exposes xiaoan as the canonical bridge and keeps hana as a legacy alias", () => { + const source = fs.readFileSync(path.join(root, "desktop", "preload.cjs"), "utf-8"); + + expect(source).toContain("const xiaoanBridge = {"); + expect(source).toContain('contextBridge.exposeInMainWorld("xiaoan", xiaoanBridge)'); + expect(source).toContain('contextBridge.exposeInMainWorld("hana", xiaoanBridge)'); + }); +}); diff --git a/tests/renderer-shell-imports.test.ts b/tests/renderer-shell-imports.test.ts new file mode 100644 index 000000000..4f3eb9d00 --- /dev/null +++ b/tests/renderer-shell-imports.test.ts @@ -0,0 +1,69 @@ +import fs from 'fs'; +import path from 'path'; +import { describe, expect, it } from 'vitest'; + +const ROOT = path.resolve(process.cwd()); +const REACT_ROOT = path.join(ROOT, 'desktop/src/react'); + +function read(filePath: string): string { + return fs.readFileSync(filePath, 'utf8'); +} + +function exists(relativePath: string): boolean { + return fs.existsSync(path.join(REACT_ROOT, relativePath)); +} + +function resolveRelativeImport(fromFile: string, importPath: string): string { + const fromDir = path.dirname(path.join(REACT_ROOT, fromFile)); + const resolved = path.normalize(path.join(fromDir, importPath)); + const candidates = [ + resolved, + `${resolved}.ts`, + `${resolved}.tsx`, + path.join(resolved, 'index.ts'), + path.join(resolved, 'index.tsx'), + ]; + return candidates.find((candidate) => fs.existsSync(candidate)) ?? resolved; +} + +function collectRelativeImports(source: string): string[] { + const imports: string[] = []; + const patterns = [ + /import\s+[^'";]+['"](\.[^'"]+)['"]/g, + /export\s+[^'";]+['"](\.[^'"]+)['"]/g, + /import\(['"](\.[^'"]+)['"]\)/g, + ]; + for (const pattern of patterns) { + for (const match of source.matchAll(pattern)) { + imports.push(match[1]); + } + } + return imports; +} + +describe('renderer shell imports', () => { + const shellFiles = [ + 'App.tsx', + 'components/app/AppPages.tsx', + 'components/app/ChatPage.tsx', + 'components/app/ChatSidebar.tsx', + 'components/chat/ChatArea.tsx', + 'components/chat/ChatShell.tsx', + 'components/WelcomeScreen.tsx', + ]; + + it.each(shellFiles)('%s only imports existing local modules', (file) => { + const source = read(path.join(REACT_ROOT, file)); + for (const importPath of collectRelativeImports(source)) { + const resolved = resolveRelativeImport(file, importPath); + expect(fs.existsSync(resolved), `${file} -> ${importPath} (${resolved})`).toBe(true); + } + }); + + it('ChatPage imports ChatArea from chat/, not app/', () => { + const source = read(path.join(REACT_ROOT, 'components/app/ChatPage.tsx')); + expect(source).toContain("from '../chat/ChatArea'"); + expect(source).not.toContain("from './ChatArea'"); + expect(exists('components/chat/ChatArea.tsx')).toBe(true); + }); +}); diff --git a/tests/server-identity.test.ts b/tests/server-identity.test.ts index 6a4169837..9331ad7bd 100644 --- a/tests/server-identity.test.ts +++ b/tests/server-identity.test.ts @@ -117,7 +117,7 @@ describe("server identity loader", () => { const serverNode = JSON.parse(fs.readFileSync(path.join(tmpDir, "server-node.json"), "utf-8")); expect(serverNode).toMatchObject({ schemaVersion: 1, - label: "Local Hana", + label: "Local Xiaoan", serverNodeId: serverNode.serverId, nodeKind: "local", transport: "loopback", diff --git a/tests/settings-ui-preferences-route.test.ts b/tests/settings-ui-preferences-route.test.ts new file mode 100644 index 000000000..3db1fc844 --- /dev/null +++ b/tests/settings-ui-preferences-route.test.ts @@ -0,0 +1,42 @@ +import { Hono } from "hono"; +import { describe, expect, it, vi } from "vitest"; +import { createPreferencesRoute } from "../server/routes/preferences.ts"; +import { normalizeSettingsUiPrefs } from "../shared/settings-ui-preferences.ts"; + +function makeApp(engine) { + const app = new Hono(); + app.route("/api", createPreferencesRoute(engine)); + return app; +} + +describe("settings UI preference routes", () => { + it("persists and returns normalized settings UI preferences", async () => { + let settingsUi = normalizeSettingsUiPrefs({}); + const engine = { + getSharedModels: vi.fn(() => ({})), + getSearchConfig: vi.fn(() => ({})), + getUtilityApi: vi.fn(() => ({})), + getSettingsUiPrefs: vi.fn(() => settingsUi), + setSettingsUiPrefs: vi.fn((partial) => { + settingsUi = normalizeSettingsUiPrefs({ ...settingsUi, ...partial }); + return settingsUi; + }), + }; + const app = makeApp(engine); + + const putRes = await app.request("/api/preferences/settings-ui", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ settingsUi: { developerModeEnabled: true } }), + }); + expect(putRes.status).toBe(200); + const putBody = await putRes.json(); + expect(putBody.ok).toBe(true); + expect(putBody.settingsUi).toEqual({ developerModeEnabled: true }); + + const getRes = await app.request("/api/preferences/settings-ui"); + expect(getRes.status).toBe(200); + const getBody = await getRes.json(); + expect(getBody.settingsUi).toEqual({ developerModeEnabled: true }); + }); +}); diff --git a/tests/settings-ui-preferences.test.ts b/tests/settings-ui-preferences.test.ts new file mode 100644 index 000000000..cb1303c2f --- /dev/null +++ b/tests/settings-ui-preferences.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { + CORE_SETTINGS_TAB_IDS, + DEVELOPER_UNLOCK_PASSPHRASE, + isSettingsTabVisible, + matchesDeveloperUnlockPassphrase, + mergeSettingsUiPrefs, + normalizeSettingsUiPrefs, +} from "../shared/settings-ui-preferences.ts"; + +describe("settings-ui-preferences", () => { + it("defaults developer mode to off", () => { + expect(normalizeSettingsUiPrefs({})).toEqual({ developerModeEnabled: false }); + expect(normalizeSettingsUiPrefs({ developerModeEnabled: true })).toEqual({ developerModeEnabled: true }); + }); + + it("matches developer passphrase", () => { + expect(matchesDeveloperUnlockPassphrase(DEVELOPER_UNLOCK_PASSPHRASE)).toBe(true); + expect(matchesDeveloperUnlockPassphrase(" XiaoAn ")).toBe(true); + expect(matchesDeveloperUnlockPassphrase("wrong")).toBe(false); + }); + + it("merges partial patches", () => { + expect(mergeSettingsUiPrefs({}, { developerModeEnabled: true })).toEqual({ developerModeEnabled: true }); + }); + + it("hides developer-only tabs when mode is off", () => { + for (const tabId of CORE_SETTINGS_TAB_IDS) { + expect(isSettingsTabVisible(tabId, false)).toBe(true); + } + expect(isSettingsTabVisible("providers", false)).toBe(true); + expect(isSettingsTabVisible("skills", false)).toBe(false); + expect(isSettingsTabVisible("providers", true)).toBe(true); + }); +}); diff --git a/tests/setup-dom-globals.ts b/tests/setup-dom-globals.ts new file mode 100644 index 000000000..748117819 --- /dev/null +++ b/tests/setup-dom-globals.ts @@ -0,0 +1,18 @@ +/** + * Polyfills for jsdom tests (matchMedia, etc.) + */ +if (typeof window !== "undefined" && typeof window.matchMedia !== "function") { + Object.defineProperty(window, "matchMedia", { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }), + }); +} diff --git a/tests/startup-contract.test.ts b/tests/startup-contract.test.ts index f5fa48b25..843055e39 100644 --- a/tests/startup-contract.test.ts +++ b/tests/startup-contract.test.ts @@ -24,13 +24,24 @@ describe("local startup contract", () => { expect(launchJs).toContain('from "./dev-env.js"'); expect(launchJs).toContain("applyDevEnvironment(process.env)"); expect(devEnvJs).toContain("HANA_DEV_NODE_BIN"); + expect(devEnvJs).toContain("defaultDevXiaoanHome"); expect(mainCjs).toContain("HANA_DEV_NODE_BIN"); const env = applyDevEnvironment({}, { nodeBin: "/tmp/hana-node" }); expect(env.HANA_DEV_NODE_BIN).toBe("/tmp/hana-node"); + expect(env.XIAOAN_HOME).toContain(".xiaoan-dev"); + expect(env.HANA_HOME).toBe(env.XIAOAN_HOME); + + const explicitXiaoanEnv = applyDevEnvironment({ XIAOAN_HOME: "/tmp/xiaoan-dev" }); + expect(explicitXiaoanEnv.XIAOAN_HOME).toBe("/tmp/xiaoan-dev"); + expect(explicitXiaoanEnv.HANA_HOME).toBe("/tmp/xiaoan-dev"); + + const legacyEnv = applyDevEnvironment({ HANA_HOME: "/tmp/hanako-dev" }); + expect(legacyEnv.XIAOAN_HOME).toBe("/tmp/hanako-dev"); + expect(legacyEnv.HANA_HOME).toBe("/tmp/hanako-dev"); }); - it("server configures Pi SDK from HANA_HOME and CLI stays server-first", () => { + it("server configures Pi SDK from Xiaoan home and CLI stays server-first", () => { const cliSource = fs.readFileSync(path.join(ROOT, "index.js"), "utf-8"); const cliEntrySource = fs.readFileSync(path.join(ROOT, "cli", "entry.ts"), "utf-8"); const launchSource = fs.readFileSync(path.join(ROOT, "scripts", "launch.js"), "utf-8"); @@ -40,6 +51,8 @@ describe("local startup contract", () => { expect(cliSource).not.toContain("HanaEngine"); expect(cliEntrySource).not.toContain("HanaEngine"); expect(launchSource).toContain('"cli/entry.ts"'); + expect(serverSource).toContain("resolveHanakoHome(process.env.XIAOAN_HOME || process.env.HANA_HOME)"); + expect(serverSource).toContain("process.env.XIAOAN_HOME = hanakoHome"); expect(serverSource).toContain("ensureHanaPiSdkDirs(hanakoHome)"); expect(serverSource).toContain("configureProcessPiSdkEnv(hanakoHome)"); }); @@ -47,11 +60,36 @@ describe("local startup contract", () => { it("desktop main propagates Hana-owned Pi SDK env to the spawned server", () => { const mainCjs = fs.readFileSync(path.join(ROOT, "desktop", "main.cjs"), "utf-8"); + expect(mainCjs).toContain("resolveHanakoHome(process.env.XIAOAN_HOME || process.env.HANA_HOME)"); expect(mainCjs).toContain("ensureHanaPiSdkDirs(hanakoHome)"); expect(mainCjs).toContain("configureProcessPiSdkEnv(hanakoHome)"); expect(mainCjs).toContain("withHanaPiSdkEnv(process.env, hanakoHome)"); }); + it("uses a short Xiaoan-branded splash instead of the old placeholder mark", () => { + const mainCjs = fs.readFileSync(path.join(ROOT, "desktop", "main.cjs"), "utf-8"); + const splashSource = fs.readFileSync(path.join(ROOT, "desktop", "src", "react", "splash", "SplashApp.tsx"), "utf-8"); + const onboardingMark = fs.readFileSync(path.join(ROOT, "desktop", "src", "react", "onboarding", "XiaoanMark.tsx"), "utf-8"); + const zhLocale = fs.readFileSync(path.join(ROOT, "desktop", "src", "locales", "zh.json"), "utf-8"); + + expect(mainCjs).toContain("const minSplashMs = 900"); + expect(splashSource).toContain('aria-label={DEFAULT_NAME}'); + expect(splashSource).not.toContain("M9 21V11"); + expect(onboardingMark).toContain("xiaoan-mark-label"); + expect(onboardingMark).not.toContain("M9 21V11"); + expect(zhLocale).toContain("正在整理记忆与能力"); + expect(zhLocale).not.toContain("载入助手与技能"); + }); + + it("serves the mobile shell as Xiaoan, not the legacy HanaAgent app", () => { + const manifestSource = fs.readFileSync(path.join(ROOT, "desktop", "src", "mobile-manifest.webmanifest"), "utf-8"); + const manifest = JSON.parse(manifestSource); + + expect(manifest.name).toBe("小安"); + expect(manifest.short_name).toBe("小安"); + expect(manifestSource).not.toContain("HanaAgent"); + }); + it("desktop main installs the client single-instance lock before app readiness", () => { const mainCjs = fs.readFileSync(path.join(ROOT, "desktop", "main.cjs"), "utf-8"); @@ -87,8 +125,12 @@ describe("local startup contract", () => { expect(buildServer).toContain("bundle/cli.js"); expect(buildServer).toContain('path.join(ROOT, "cli", "entry.ts")'); - expect(buildServer).toContain('path.join(outDir, "hana")'); + expect(buildServer).toContain('["xiaoan", "hana"]'); + expect(buildServer).toContain('["xiaoan-server", "hana-server"]'); + expect(buildServer).toContain('path.join(outDir, "xiaoan.cmd")'); + expect(buildServer).toContain('path.join(outDir, "xiaoan-server.cmd")'); expect(buildServer).toContain('path.join(outDir, "hana.cmd")'); + expect(buildServer).toContain('path.join(outDir, "hana-server.cmd")'); }); it("server dependency install explicitly enables native package scripts", () => { diff --git a/tests/telegram-adapter-media.test.ts b/tests/telegram-adapter-media.test.ts index 789132e9b..e3da47ac2 100644 --- a/tests/telegram-adapter-media.test.ts +++ b/tests/telegram-adapter-media.test.ts @@ -4,14 +4,16 @@ const botInstances = []; vi.mock("node-telegram-bot-api", () => { class MockTelegramBot { - declare _request: any; declare getMe: any; declare on: any; declare removeAllListeners: any; declare sendAudio: any; declare sendDocument: any; + declare sendMessageDraft: any; declare sendMessage: any; declare sendPhoto: any; + declare sendRichMessage: any; + declare sendRichMessageDraft: any; declare sendVideo: any; declare stopPolling: any; constructor() { @@ -23,7 +25,9 @@ vi.mock("node-telegram-bot-api", () => { this.sendVideo = vi.fn(async () => {}); this.sendAudio = vi.fn(async () => {}); this.sendDocument = vi.fn(async () => {}); - this._request = vi.fn(async () => {}); + this.sendMessageDraft = vi.fn(async () => {}); + this.sendRichMessageDraft = vi.fn(async () => {}); + this.sendRichMessage = vi.fn(async () => {}); this.getMe = vi.fn(async () => ({ username: "hana" })); botInstances.push(this); } @@ -152,14 +156,12 @@ describe("createTelegramAdapter media delivery", () => { messageThreadId: 67890, }); - expect(bot._request).toHaveBeenCalledWith("sendMessageDraft", { - form: { - chat_id: "chat-1", - draft_id: 12345, - message_thread_id: 67890, - text: "streaming text", - }, - }); + expect(bot.sendMessageDraft).toHaveBeenCalledWith( + "chat-1", + 12345, + "streaming text", + { message_thread_id: 67890 }, + ); adapter.stop(); }); @@ -182,23 +184,19 @@ describe("createTelegramAdapter media delivery", () => { messageThreadId: 67890, }); - expect(bot._request).toHaveBeenNthCalledWith(1, "sendRichMessageDraft", { - form: { - chat_id: "chat-1", - draft_id: 12345, - message_thread_id: 67890, - rich_message: JSON.stringify({ markdown: "**streaming**" }), - }, - }); - expect(bot._request).toHaveBeenNthCalledWith(2, "sendRichMessage", { - form: { - chat_id: "chat-1", - message_thread_id: 67890, - rich_message: JSON.stringify({ - markdown: "# Final\n\n
Morex
", - }), + expect(bot.sendRichMessageDraft).toHaveBeenCalledWith( + "chat-1", + 12345, + { markdown: "**streaming**" }, + { message_thread_id: 67890 }, + ); + expect(bot.sendRichMessage).toHaveBeenCalledWith( + "chat-1", + { + markdown: "# Final\n\n
Morex
", }, - }); + { message_thread_id: 67890 }, + ); adapter.stop(); }); }); diff --git a/tests/update-settings-tool.test.ts b/tests/update-settings-tool.test.ts index ece1e4af6..1880a6528 100644 --- a/tests/update-settings-tool.test.ts +++ b/tests/update-settings-tool.test.ts @@ -238,17 +238,17 @@ describe("update-settings-tool", () => { }); }); - describe("bridge media public URL", () => { - it("searches the Bridge media public URL setting", async () => { + describe("channel media public URL", () => { + it("searches the Channel media public URL setting", async () => { const { tool } = buildTool(); const result = await tool.execute("c-bridge-url-search", { action: "search", query: "public url" }); const text = result.content[0].text; expect(text).toContain("bridge_media_public_base_url"); - expect(text).toContain("Bridge File Public URL"); + expect(text).toContain("Channel File Public URL"); }); - it("applies the Bridge media public URL as a global preference", async () => { + it("applies the Channel media public URL as a global preference", async () => { const { tool, engine } = buildTool(); await tool.execute("c-bridge-url-apply", { action: "apply", @@ -518,11 +518,11 @@ describe("update-settings-tool", () => { expect(text).not.toContain("claude-design"); }); - it("search 'theme' 结果中 options 包含全部 16 个选项(15 主题 + auto)", async () => { + it("search 'theme' 结果中 options 包含全部 14 个选项(13 主题 + auto)", async () => { const { tool } = buildTool(); const result = await tool.execute("c10", { action: "search", query: "theme" }); const text = result.content[0].text; - expect(reg.getAllUIOptions()).toHaveLength(16); + expect(reg.getAllUIOptions()).toHaveLength(14); // 搜索结果会截断,抽查首尾可见项 expect(text).toContain("xiaoan-console"); expect(text).toContain("auto"); diff --git a/tests/windows-icon-contract.test.ts b/tests/windows-icon-contract.test.ts index 87f7caac9..eccc358c3 100644 --- a/tests/windows-icon-contract.test.ts +++ b/tests/windows-icon-contract.test.ts @@ -61,7 +61,7 @@ describe("Windows icon contract", () => { const main = fs.readFileSync(path.join(ROOT, "desktop", "main.cjs"), "utf-8"); expect(main).toContain("app.setAppUserModelId"); - expect(main).toContain("com.hanako.app"); + expect(main).toContain("com.xiaoan.agent"); expect(main).toContain('"icon.ico"'); expect(main).toContain('"tray.ico"'); expect(main).not.toMatch(/titleBarOpts[\s\S]*?"tray\.ico"[\s\S]*?return\s+\{\s*frame:\s*false,\s*icon:/); diff --git a/tests/windows-installer-contract.test.ts b/tests/windows-installer-contract.test.ts index 761dd55f2..e59b21908 100644 --- a/tests/windows-installer-contract.test.ts +++ b/tests/windows-installer-contract.test.ts @@ -116,10 +116,17 @@ describe("Windows NSIS installer contract", () => { it("overrides app-running detection to close HanaAgent, legacy Hanako, and the bundled server explicitly", () => { const source = fs.readFileSync(path.join(root, "build", "installer.nsh"), "utf-8"); const macro = extractMacro(source, "customCheckAppRunning"); - - expect(macro).toContain("HanaAgent.exe"); - expect(macro).toContain("Hanako.exe"); - expect(macro).toContain("hana-server.exe"); + const finder = extractMacro(source, "hanakoFindRunningProcesses"); + const killer = extractMacro(source, "hanakoKillRunningProcesses"); + + expect(finder).toContain("Xiaoan.exe"); + expect(finder).toContain("HanaAgent.exe"); + expect(finder).toContain("Hanako.exe"); + expect(finder).toContain("xiaoan-server.exe"); + expect(finder).toContain("hana-server.exe"); + expect(killer).toContain("Xiaoan.exe"); + expect(killer).toContain("xiaoan-server.exe"); + expect(killer).toContain("hana-server.exe"); expect(macro).toContain("appCannotBeClosed"); expect(macro).toContain("MB_RETRYCANCEL"); expect(macro).toContain("DetailPrint"); @@ -141,8 +148,8 @@ describe("Windows NSIS installer contract", () => { it("pins the Windows executable name to the current product identity", () => { const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8")); - expect(pkg.build.win.executableName).toBe("HanaAgent"); - expect(pkg.build.nsis.shortcutName).toBe("HanaAgent"); + expect(pkg.build.win.executableName).toBe("Xiaoan"); + expect(pkg.build.nsis.shortcutName).toBe("小安"); }); it("runs an install surface self-check and writes diagnostics before aborting", () => { @@ -155,7 +162,7 @@ describe("Windows NSIS installer contract", () => { expect(verify).toContain('$INSTDIR\\${APP_EXECUTABLE_FILENAME}'); expect(verify).toContain('$INSTDIR\\resources\\app.asar'); expect(verify).toContain('$INSTDIR\\resources\\app-update.yml'); - expect(verify).toContain('$INSTDIR\\resources\\server\\hana-server.exe'); + expect(verify).toContain('$INSTDIR\\resources\\server\\xiaoan-server.exe'); expect(verify).toContain('$INSTDIR\\resources\\server\\bootstrap.js'); expect(verify).toContain('$INSTDIR\\resources\\server\\bundle\\index.js'); expect(verify).toContain('$INSTDIR\\resources\\server\\node_modules\\better-sqlite3\\build\\Release\\better_sqlite3.node'); diff --git a/tests/xiaoan-builtin-relay.test.ts b/tests/xiaoan-builtin-relay.test.ts new file mode 100644 index 000000000..51b6069be --- /dev/null +++ b/tests/xiaoan-builtin-relay.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { + XIAOAN_BUILTIN_RELAY_PROVIDER_ID, + filterProvidersForBuiltinRelay, + getBuiltinRelayBaseUrl, + getBuiltinRelayDisplayName, + getBuiltinRelayRechargeUrl, + getBuiltinRelayConsoleUrl, + getBuiltinRelayTokenUrl, + isBuiltinRelayProvider, +} from "../shared/xiaoan-builtin-relay.ts"; + +describe("xiaoan-builtin-relay", () => { + it("exposes the anxin relay provider id", () => { + expect(XIAOAN_BUILTIN_RELAY_PROVIDER_ID).toBe("anxin-relay"); + expect(isBuiltinRelayProvider("anxin-relay")).toBe(true); + expect(isBuiltinRelayProvider("openai")).toBe(false); + }); + + it("provides localized display names", () => { + expect(getBuiltinRelayDisplayName(true)).toContain("安芯跃动"); + expect(getBuiltinRelayDisplayName(false)).toMatch(/Anxin Relay/i); + }); + + it("reads the default relay base URL from fork config", () => { + expect(getBuiltinRelayBaseUrl()).toBe("https://an520.xin/v1"); + }); + + it("reads the relay recharge URL from fork config", () => { + expect(getBuiltinRelayRechargeUrl()).toBe("https://an520.xin/console/topup"); + }); + + it("reads relay console and token URLs from fork config", () => { + expect(getBuiltinRelayConsoleUrl()).toBe("https://an520.xin/console"); + expect(getBuiltinRelayTokenUrl()).toBe("https://an520.xin/console/token"); + }); + + it("filters provider maps to the builtin relay only", () => { + expect(filterProvidersForBuiltinRelay({ + "anxin-relay": { api_key: "k" }, + openai: { api_key: "x" }, + })).toEqual({ + "anxin-relay": { api_key: "k" }, + }); + }); +}); diff --git a/tests/xiaoan-fork-migrate.test.ts b/tests/xiaoan-fork-migrate.test.ts index ce0d78872..442d91e5f 100644 --- a/tests/xiaoan-fork-migrate.test.ts +++ b/tests/xiaoan-fork-migrate.test.ts @@ -39,11 +39,25 @@ describe("xiaoan fork migrate", () => { expect(report.migrated).toBe(true); expect(report.changes.some((c) => c.includes("agent.name"))).toBe(true); - expect((cfg.agent as Record).name).toBe("小小安"); + expect((cfg.agent as Record).name).toBe("小安"); expect((cfg.agent as Record).yuan).toBe("xiaoan"); expect((cfg.skills as Record).enabled).toEqual(["xiaoan-handbook", "skill-creator"]); }); + it("normalizes previous Xiaoan fork placeholder names without touching custom names", () => { + writeConfig({ + agent: { name: "小小安", yuan: "xiaoan" }, + skills: { enabled: ["xiaoan-handbook", "skill-creator"] }, + }); + + const report = migrateXiaoanForkDefaults(hanakoHome); + const cfg = YAML.load(fs.readFileSync(path.join(hanakoHome, "agents", "hanako", "config.yaml"), "utf-8")) as Record; + + expect(report.migrated).toBe(true); + expect(report.changes.some((c) => c.includes("agent.name"))).toBe(true); + expect((cfg.agent as Record).name).toBe("小安"); + }); + it("does not overwrite a custom agent display name", () => { writeConfig({ agent: { name: "My Copilot", yuan: "ming" }, diff --git a/tests/xiaoan-fork.test.ts b/tests/xiaoan-fork.test.ts index 1911829f4..ba154d791 100644 --- a/tests/xiaoan-fork.test.ts +++ b/tests/xiaoan-fork.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from "vitest"; import { readFileSync } from "fs"; -import { getPlatformIdentityPrompt, getDefaultPluginMarketplaceUrl, getUpdateFeed, isAutoUpdateEnabled, XIAOAN_FORK } from "../shared/xiaoan-fork.ts"; +import { getPlatformIdentityPrompt, getDefaultPluginMarketplaceUrl, getUpdateFeed, getProviderUserAgent, getBotUserAgent, getProvidersYamlHeaderZh, isAutoUpdateEnabled, XIAOAN_FORK } from "../shared/xiaoan-fork.ts"; describe("xiaoan fork config", () => { - it("defaults auto-update off for independent fork", () => { + it("enables Xiaoan-owned update feed while keeping automatic checks off by default", () => { delete process.env.XIAOAN_AUTO_UPDATE; - expect(XIAOAN_FORK.autoUpdate.enabled).toBe(false); + expect(XIAOAN_FORK.autoUpdate.enabled).toBe(true); expect(XIAOAN_FORK.defaultAutoCheckUpdates).toBe(false); - expect(isAutoUpdateEnabled()).toBe(false); + expect(isAutoUpdateEnabled()).toBe(true); }); it("points update feed at An-Agent repo when enabled via env", () => { @@ -25,10 +25,17 @@ describe("xiaoan fork config", () => { it("injects xiaoan platform identity into system prompt text", () => { expect(getPlatformIdentityPrompt(true)).toContain("小安"); - expect(getPlatformIdentityPrompt(true)).toContain("小小安"); + expect(getPlatformIdentityPrompt(true)).not.toContain("小小安"); expect(getPlatformIdentityPrompt(true)).toContain("xiaoan-core"); expect(getPlatformIdentityPrompt(false)).toContain("Xiaoan"); - expect(getPlatformIdentityPrompt(false)).toContain("Minian"); + expect(getPlatformIdentityPrompt(false)).not.toContain("Minian"); + }); + + it("exposes distribution user agents from fork config", () => { + expect(getProviderUserAgent()).toBe("Xiaoan/1.0"); + expect(getBotUserAgent()).toBe("XiaoanBot/1.0"); + expect(getProvidersYamlHeaderZh()).toContain("小安"); + expect(XIAOAN_FORK.distribution.userAgent).toBe("Xiaoan/1.0"); }); it("points distribution and update metadata at An-Agent, not upstream openhanako", () => { @@ -39,6 +46,8 @@ describe("xiaoan fork config", () => { expect(getDefaultPluginMarketplaceUrl()).toContain("marketplace.json"); const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")); + expect(pkg.bin.xiaoan).toBe("cli/entry.ts"); + expect(pkg.bin.hana).toBe("cli/entry.ts"); expect(pkg.scripts["install:local"]).not.toContain("openhanako"); expect(pkg.scripts["install:local"]).toContain("An-Agent"); expect(pkg.build.publish[0].repo).toBe("An-Agent"); diff --git a/tests/xiaoan-gsap-chat-motion.test.ts b/tests/xiaoan-gsap-chat-motion.test.ts new file mode 100644 index 000000000..0afc0aefb --- /dev/null +++ b/tests/xiaoan-gsap-chat-motion.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +describe('xiaoan chat GSAP motion', () => { + it('does not animate the full chat-area shell when session panels are visible', () => { + const chatShell = fs.readFileSync( + path.join(process.cwd(), 'desktop/src/react/components/chat/ChatShell.tsx'), + 'utf8', + ); + const gsap = fs.readFileSync( + path.join(process.cwd(), 'desktop/src/react/motion/xiaoan-gsap.ts'), + 'utf8', + ); + + expect(chatShell).toMatch(/data-gsap-chat-enter=\{hasPanels \? undefined : ''\}/); + expect(gsap).toMatch(/:not\(\.chat-area\.has-panels\)/); + expect(gsap).toMatch(/releaseAppShellMotion/); + expect(gsap).toMatch(/onInterrupt:\s*\(\)\s*=>\s*releaseAppShellMotion/); + expect(gsap).toMatch(/MOTION_CLEAR_PROPS/); + expect(gsap).toMatch(/releaseChatBlockMotion/); + }); + + it('cleans up chat motion with kill + clearProps instead of context revert', () => { + const chatShell = fs.readFileSync( + path.join(process.cwd(), 'desktop/src/react/components/chat/ChatShell.tsx'), + 'utf8', + ); + + expect(chatShell).toContain('releaseChatBlockMotion(container)'); + expect(chatShell).not.toContain('ctx?.revert()'); + }); +}); diff --git a/tests/xiaoan-gsap-shell-motion.test.ts b/tests/xiaoan-gsap-shell-motion.test.ts new file mode 100644 index 000000000..ed8672974 --- /dev/null +++ b/tests/xiaoan-gsap-shell-motion.test.ts @@ -0,0 +1,47 @@ +// @vitest-environment jsdom + +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import gsap from 'gsap'; +import { + animateAppShellEnter, + releaseAppShellMotion, +} from '../desktop/src/react/motion/xiaoan-gsap.ts'; + +describe('xiaoan app shell GSAP motion', () => { + it('releaseAppShellMotion restores opacity after interrupted enter', () => { + document.body.innerHTML = ` +
+
+ +
+
+ `; + const root = document.querySelector('.app-shell') as HTMLElement; + const main = document.querySelector('.main-content') as HTMLElement; + + const tl = animateAppShellEnter(root); + expect(tl).not.toBeNull(); + tl?.progress(0.05); + + const midOpacity = Number.parseFloat(getComputedStyle(main).opacity); + expect(midOpacity).toBeLessThan(1); + + tl?.kill(); + releaseAppShellMotion(root); + + expect(getComputedStyle(root).opacity).toBe('1'); + expect(getComputedStyle(main).opacity).toBe('1'); + gsap.globalTimeline.clear(); + }); + + it('does not fade the entire app-shell root (only child regions)', () => { + const source = fs.readFileSync( + path.join(process.cwd(), 'desktop/src/react/motion/xiaoan-gsap.ts'), + 'utf8', + ); + expect(source).not.toMatch(/\.from\(root,\s*\{\s*opacity:\s*0/); + expect(source).toContain('releaseAppShellMotion'); + }); +}); diff --git a/tests/xiaoan-layout-theme-contract.test.ts b/tests/xiaoan-layout-theme-contract.test.ts new file mode 100644 index 000000000..3d205d3d9 --- /dev/null +++ b/tests/xiaoan-layout-theme-contract.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import reg from '../desktop/src/shared/theme-registry.cjs'; + +const LAYOUT_CSS_FILES = [ + 'desktop/src/design/xiaoan-layout.css', + 'desktop/src/design/xiaoan-console-shell.css', + 'desktop/src/design/xiaoan-platform.css', +]; + +const THEME_CSS_FILES = [ + 'desktop/src/themes/xiaoan-console.css', + 'desktop/src/themes/xiaoan-console-dark.css', +]; + +describe('xiaoan layout theme contract', () => { + it('layout CSS covers every legacy console theme alias', () => { + const legacyIds = Object.keys(reg.LEGACY_THEME_ALIASES); + expect(legacyIds.length).toBeGreaterThan(0); + + for (const relPath of LAYOUT_CSS_FILES) { + const css = readFileSync(join(process.cwd(), relPath), 'utf8'); + for (const legacyId of legacyIds) { + expect(css, `${relPath} missing [data-theme="${legacyId}"]`).toContain( + `[data-theme="${legacyId}"]`, + ); + } + } + }); + + it('layout CSS sets flex-footer chat input mode for console themes', () => { + const css = readFileSync( + join(process.cwd(), 'desktop/src/design/xiaoan-layout.css'), + 'utf8', + ); + expect(css).toContain('--xiaoan-chat-input-layout: flex-footer'); + expect(css).toMatch(/html:is\([^)]*xiaoan-claude[^)]*\)/); + }); + + it('console theme color sheets include legacy light/dark ids', () => { + const light = readFileSync( + join(process.cwd(), THEME_CSS_FILES[0]), + 'utf8', + ); + const dark = readFileSync( + join(process.cwd(), THEME_CSS_FILES[1]), + 'utf8', + ); + + expect(light).toContain('[data-theme="xiaoan-claude"]'); + expect(dark).toContain('[data-theme="xiaoan-claude-dark"]'); + }); +}); diff --git a/tests/xiaoan-runtime-migrate.test.ts b/tests/xiaoan-runtime-migrate.test.ts index 6ac506cd8..479a9c4b3 100644 --- a/tests/xiaoan-runtime-migrate.test.ts +++ b/tests/xiaoan-runtime-migrate.test.ts @@ -39,6 +39,23 @@ describe("xiaoan runtime migrate", () => { expect(fs.existsSync(legacy)).toBe(false); }); + it("does not rename default homes when XIAOAN_HOME or HANA_HOME is explicit", () => { + const legacy = path.join(root, ".hanako"); + const canonical = path.join(root, ".xiaoan"); + fs.mkdirSync(legacy, { recursive: true }); + + process.env.XIAOAN_HOME = path.join(root, "explicit-xiaoan"); + expect(migrateDefaultHomeDirectory(root)).toBeNull(); + expect(fs.existsSync(legacy)).toBe(true); + expect(fs.existsSync(canonical)).toBe(false); + + delete process.env.XIAOAN_HOME; + process.env.HANA_HOME = path.join(root, "explicit-hana"); + expect(migrateDefaultHomeDirectory(root)).toBeNull(); + expect(fs.existsSync(legacy)).toBe(true); + expect(fs.existsSync(canonical)).toBe(false); + }); + it("resolveXiaoanHome prefers canonical directory when both exist", () => { fs.mkdirSync(path.join(root, ".xiaoan")); fs.mkdirSync(path.join(root, ".hanako")); @@ -78,6 +95,41 @@ describe("xiaoan runtime migrate", () => { expect(prefs.primaryAgent).toBe(CANONICAL_AGENT_ID); }); + it("renames the legacy agent directory without overwriting agent config or provider files", () => { + const home = path.join(root, ".xiaoan"); + const legacyAgentDir = path.join(home, "agents", LEGACY_AGENT_ID); + fs.mkdirSync(legacyAgentDir, { recursive: true }); + fs.writeFileSync( + path.join(legacyAgentDir, "config.yaml"), + "agent:\n name: Custom Hanako\nmodels:\n chat: custom/model\n", + "utf-8", + ); + fs.writeFileSync( + path.join(home, "added-models.yaml"), + "providers:\n custom-provider:\n base_url: https://example.invalid/v1\n api: openai-completions\n", + "utf-8", + ); + fs.writeFileSync( + path.join(home, "provider-catalog.json"), + JSON.stringify({ catalogVersion: 2, providers: { custom: { id: "custom" } } }, null, 2) + "\n", + "utf-8", + ); + + migrateXiaoanRuntime(home); + + const canonicalAgentDir = path.join(home, "agents", CANONICAL_AGENT_ID); + expect(fs.readFileSync(path.join(canonicalAgentDir, "config.yaml"), "utf-8")).toBe( + "agent:\n name: Custom Hanako\nmodels:\n chat: custom/model\n", + ); + expect(fs.readFileSync(path.join(home, "added-models.yaml"), "utf-8")).toBe( + "providers:\n custom-provider:\n base_url: https://example.invalid/v1\n api: openai-completions\n", + ); + expect(JSON.parse(fs.readFileSync(path.join(home, "provider-catalog.json"), "utf-8"))).toEqual({ + catalogVersion: 2, + providers: { custom: { id: "custom" } }, + }); + }); + it("resolvePrimaryAgentId returns minian after migration", () => { const home = path.join(root, ".xiaoan"); fs.mkdirSync(path.join(home, "agents", CANONICAL_AGENT_ID), { recursive: true }); @@ -90,9 +142,12 @@ describe("xiaoan runtime migrate", () => { fs.mkdirSync(home, { recursive: true }); const first = migrateXiaoanRuntime(home); + const marker = path.join(home, "user", "xiaoan-runtime-migration.json"); + const firstMarker = fs.readFileSync(marker, "utf-8"); const second = migrateXiaoanRuntime(home); expect(first.migrated).toBe(false); expect(second.migrated).toBe(false); expect(second.changes).toEqual([]); + expect(fs.readFileSync(marker, "utf-8")).toBe(firstMarker); }); }); diff --git a/tests/xiaoan-settings-product-notes.test.ts b/tests/xiaoan-settings-product-notes.test.ts new file mode 100644 index 000000000..215be6249 --- /dev/null +++ b/tests/xiaoan-settings-product-notes.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { + XIAOAN_BUILTIN_RELAY_NOTES, + XIAOAN_DEVELOPER_MODE_NOTES, + XIAOAN_SETTINGS_SECTION_GATING, +} from "../shared/xiaoan-settings-product-notes.ts"; +import { + BUILTIN_RELAY_KNOWN_ISSUE_KEYS, + DEVELOPER_MODE_KNOWN_ISSUE_KEYS, +} from "../desktop/src/react/settings/xiaoan-settings-annotations.ts"; + +describe("xiaoan-settings-product-notes", () => { + it("documents non-empty maintainer notes for relay and developer mode", () => { + expect(XIAOAN_BUILTIN_RELAY_NOTES.knownIssues.length).toBeGreaterThan(3); + expect(XIAOAN_DEVELOPER_MODE_NOTES.knownIssues.length).toBeGreaterThan(2); + expect(XIAOAN_SETTINGS_SECTION_GATING.providers.core.length).toBeGreaterThan(0); + }); + + it("keeps UI issue keys aligned with documented relay issues", () => { + expect(BUILTIN_RELAY_KNOWN_ISSUE_KEYS.length).toBeGreaterThanOrEqual(5); + expect(DEVELOPER_MODE_KNOWN_ISSUE_KEYS.length).toBe(3); + }); +}); diff --git a/tests/yuan-visuals.test.ts b/tests/yuan-visuals.test.ts index 684bdb9e0..967014633 100644 --- a/tests/yuan-visuals.test.ts +++ b/tests/yuan-visuals.test.ts @@ -3,28 +3,34 @@ import { getYuanVisual, moodLabelForYuan, normalizeYuan } from "../shared/yuan-v describe("yuan visuals", () => { it("keeps the desktop and CLI yuan symbolism in one place", () => { + expect(getYuanVisual("xiaoan")).toMatchObject({ + symbol: "", + moodLabel: "状态", + accent: "#E8A4B8", + avatar: "Xiaoan.png", + }); expect(getYuanVisual("hanako")).toMatchObject({ - symbol: "✿", - moodLabel: "MOOD", - accent: "#537D96", - avatar: "Hanako.png", + symbol: "", + moodLabel: "状态", + accent: "#86909c", + avatar: "", }); expect(getYuanVisual("butter")).toMatchObject({ - symbol: "❊", - moodLabel: "PULSE", - accent: "#5BA88C", + symbol: "❋", + moodLabel: "心绪", + accent: "#C47A6E", avatar: "Butter.png", }); expect(getYuanVisual("ming")).toMatchObject({ symbol: "◈", - moodLabel: "REFLECT", - accent: "#8BA4B4", + moodLabel: "沉思", + accent: "#4A5F7A", avatar: "Ming.png", }); }); - it("falls back to hanako for unknown yuan values", () => { - expect(normalizeYuan("unknown")).toBe("hanako"); - expect(moodLabelForYuan("unknown")).toBe("✿ MOOD"); + it("falls back to xiaoan for unknown yuan values", () => { + expect(normalizeYuan("unknown")).toBe("xiaoan"); + expect(moodLabelForYuan("unknown")).toBe("状态"); }); }); diff --git a/tests/zip-writer-roundtrip.test.ts b/tests/zip-writer-roundtrip.test.ts new file mode 100644 index 000000000..e83eb0a26 --- /dev/null +++ b/tests/zip-writer-roundtrip.test.ts @@ -0,0 +1,54 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { describe, expect, it } from "vitest"; +import { extractZip } from "../lib/extract-zip.ts"; +import { writeZipFromDirectory } from "../lib/zip-writer.ts"; + +const HANAKO_AVATAR = path.join("desktop", "src", "assets", "Hanako.png"); + +describe("zip-writer roundtrip", () => { + it("writes archives that extract-zip can read", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "xiaoan-zip-roundtrip-")); + try { + const source = path.join(root, "source"); + const zipPath = path.join(root, "out.zip"); + const dest = path.join(root, "dest"); + fs.mkdirSync(path.join(source, "nested"), { recursive: true }); + fs.writeFileSync(path.join(source, "card.json"), "{\"ok\":true}", "utf-8"); + fs.writeFileSync(path.join(source, "nested", "inner.txt"), "hello", "utf-8"); + + await writeZipFromDirectory(source, zipPath); + fs.mkdirSync(dest, { recursive: true }); + await extractZip(zipPath, dest); + + expect(fs.readFileSync(path.join(dest, "card.json"), "utf-8")).toBe("{\"ok\":true}"); + expect(fs.readFileSync(path.join(dest, "nested", "inner.txt"), "utf-8")).toBe("hello"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it("writes archives with multiple png assets that extract-zip can read", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "xiaoan-zip-multi-png-")); + try { + const source = path.join(root, "source"); + const zipPath = path.join(root, "out.zip"); + const dest = path.join(root, "dest"); + fs.mkdirSync(path.join(source, "assets"), { recursive: true }); + fs.copyFileSync(HANAKO_AVATAR, path.join(source, "assets", "avatar.png")); + fs.copyFileSync(HANAKO_AVATAR, path.join(source, "assets", "card-front.png")); + fs.copyFileSync(path.join("desktop", "src", "assets", "character-cards", "yuan-hanako-card-back.png"), path.join(source, "assets", "card-back.png")); + fs.writeFileSync(path.join(source, "card.json"), "{\"ok\":true}", "utf-8"); + + await writeZipFromDirectory(source, zipPath); + fs.mkdirSync(dest, { recursive: true }); + await extractZip(zipPath, dest); + + expect(fs.existsSync(path.join(dest, "assets", "avatar.png"))).toBe(true); + expect(fs.existsSync(path.join(dest, "assets", "card-front.png"))).toBe(true); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/vitest.config.js b/vitest.config.js index 42f2388d0..ecc2d4c5e 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -22,7 +22,7 @@ export default defineConfig({ "dist-computer-use/**", ], testTimeout: 10_000, - setupFiles: ["./tests/setup-auto-updater.ts"], + setupFiles: ["./tests/setup-dom-globals.ts", "./tests/setup-auto-updater.ts"], server: { deps: { inline: ["electron-updater", /desktop\/auto-updater/],