From 36b0296730141048a1637a4d9b11e68dd7d50a2c Mon Sep 17 00:00:00 2001 From: chiguyong Date: Thu, 2 Jul 2026 01:07:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A7=81=E8=91=A3=E4=BC=9A=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=8C=81=E4=B9=85=E5=8C=96=E4=BF=AE=E5=A4=8D=20+=20em?= =?UTF-8?q?oji=20=E7=A7=BB=E9=99=A4=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 board_started/expert_speech/round_summary/board_concluded 事件持久化 - 添加 is_board 标记到会话列表和详情接口 - 实现 restoreBoardStateFromMessages 从持久化消息恢复 boardState - 添加 ChatSidebar 私董会徽章 - 添加 emoji 移除计划文档 (docs/plans/2026-07-02-001) --- agentkit.yaml | 16 +- configs/skills/benchmark_runner.yaml | 3 +- ...7-02-001-refactor-remove-all-emoji-plan.md | 405 ++++++++++ src/agentkit/bitable/models.py | 5 +- src/agentkit/bitable/repository.py | 2 +- src/agentkit/bitable/service.py | 2 +- .../chat/sqlite_conversation_store.py | 39 + src/agentkit/cli/admin.py | 4 +- src/agentkit/cli/admin_client.py | 2 +- src/agentkit/cli/init.py | 2 +- src/agentkit/cli/main.py | 8 +- src/agentkit/cli/onboarding.py | 2 +- src/agentkit/cli/pair.py | 2 +- src/agentkit/cli/templates.py | 8 +- src/agentkit/client/sync.py | 4 +- src/agentkit/experts/board_orchestrator.py | 174 ++++- src/agentkit/memory/adapters/generic_http.py | 2 +- src/agentkit/memory/http_rag.py | 4 +- src/agentkit/server/app.py | 19 +- src/agentkit/server/auth/jwt_utils.py | 5 + src/agentkit/server/client.py | 2 +- src/agentkit/server/config.py | 4 +- src/agentkit/server/frontend/components.d.ts | 2 + .../e2e/calendar-data-consistency.spec.ts | 2 +- .../e2e/conversation-management.spec.ts | 2 +- .../server/frontend/e2e/global-setup.ts | 2 +- src/agentkit/server/frontend/e2e/helpers.ts | 18 +- .../server/frontend/playwright.config.ts | 14 +- .../server/frontend/src-tauri/src/lib.rs | 10 +- .../server/frontend/src-tauri/tauri.conf.json | 2 +- src/agentkit/server/frontend/src/App.vue | 2 +- src/agentkit/server/frontend/src/api/types.ts | 704 ++++++++++-------- .../src/components/bitable/AttachmentCell.vue | 2 +- .../src/components/bitable/BitableGrid.vue | 6 +- .../components/bitable/FieldManagePanel.vue | 2 +- .../src/components/bitable/FileCard.vue | 12 +- .../components/bitable/FileCreateModal.vue | 68 +- .../src/components/bitable/TableViewList.vue | 4 +- .../src/components/bitable/bitableIcons.ts | 102 +++ .../src/components/chat/ChatSidebar.vue | 30 +- .../components/chat/helpers/expertIdentity.ts | 70 ++ .../chat/helpers/useMessageRenderer.ts | 13 +- .../components/chat/messages/TeamPlanCard.vue | 67 +- .../components/chat/messages/UserBubble.vue | 199 ++++- .../src/components/layout/SplashScreen.vue | 6 +- .../components/layout/SystemMonitorPanel.vue | 29 +- .../components/layout/tabs/KnowledgeTab.vue | 2 +- .../src/components/layout/tabs/SkillsTab.vue | 2 +- .../src/components/layout/tabs/SystemTab.vue | 21 +- .../components/preview/ChatPreviewShell.vue | 5 +- .../server/frontend/src/stores/auth.ts | 4 +- .../server/frontend/src/stores/bitable.ts | 12 +- .../server/frontend/src/stores/chatStore.ts | 92 ++- .../server/frontend/src/stores/chatStream.ts | 178 ++++- .../server/frontend/src/stores/settings.ts | 4 +- .../server/frontend/src/stores/theme.ts | 37 +- .../server/frontend/src/styles/theme.ts | 5 + .../server/frontend/src/styles/tokens.css | 5 +- .../src/views/BitableFileDetailView.vue | 11 +- .../src/views/BitableFileListView.vue | 65 +- .../server/frontend/src/views/ChatView.vue | 13 +- .../tests/unit/stores/chatStore.test.ts | 173 +++++ src/agentkit/server/frontend/vite.config.ts | 23 +- src/agentkit/server/routes/auth.py | 10 +- src/agentkit/server/routes/bitable.py | 6 +- src/agentkit/server/routes/chat.py | 94 ++- src/agentkit/server/routes/portal.py | 242 +++++- .../static/assets/AppLayout-D3vb9nEe.css | 1 - src/agentkit/server/static/index.html | 4 +- src/agentkit/tools/bitable_tool.py | 2 +- tests/conftest.py | 8 + .../server/test_port_config_regression.py | 174 +++++ 72 files changed, 2729 insertions(+), 546 deletions(-) create mode 100644 docs/plans/2026-07-02-001-refactor-remove-all-emoji-plan.md create mode 100644 src/agentkit/server/frontend/src/components/bitable/bitableIcons.ts create mode 100644 src/agentkit/server/frontend/src/components/chat/helpers/expertIdentity.ts create mode 100644 src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts delete mode 100644 src/agentkit/server/static/assets/AppLayout-D3vb9nEe.css create mode 100644 tests/unit/server/test_port_config_regression.py diff --git a/agentkit.yaml b/agentkit.yaml index 553bab6..ef3f66f 100644 --- a/agentkit.yaml +++ b/agentkit.yaml @@ -1,6 +1,6 @@ server: host: 0.0.0.0 - port: 8001 + port: 18001 workers: 1 rate_limit: 60 llm: @@ -64,9 +64,17 @@ fallback_chain: # whitelist_override: # optional, merges with default (override wins) # planning: [search, read_file, shell] # building: [write_file, shell, read_file] -session: {backend: memory} -bus: {backend: memory} -task_store: {backend: memory} +# G10/U5: Use Redis for bus / session / task_store when REDIS_URL is set. +# Falls back to in-memory when REDIS_URL is unset (development fallback). +# The local dev environment runs pms-redis on 127.0.0.1:6379 (see .env.dev). +# Tests run in-memory for isolation; production / staging should use Redis. +session: + backend: ${AGENTKIT_SESSION_BACKEND:-memory} +bus: + backend: ${AGENTKIT_BUS_BACKEND:-memory} + redis_url: ${REDIS_URL:-redis://127.0.0.1:6379/0} +task_store: + backend: ${AGENTKIT_TASK_STORE_BACKEND:-memory} skills: {auto_discover: true, paths: ["./configs/skills"]} experts: {paths: ["./configs/experts"]} board: {max_rounds: 5, default_template: private_board, parallel_speech: true, history_compression_threshold: 20} diff --git a/configs/skills/benchmark_runner.yaml b/configs/skills/benchmark_runner.yaml index 85c3238..c4855f5 100644 --- a/configs/skills/benchmark_runner.yaml +++ b/configs/skills/benchmark_runner.yaml @@ -200,8 +200,7 @@ llm: tools: - shell - - file_read - - file_write + - read_file quality_gate: required_fields: ["content"] diff --git a/docs/plans/2026-07-02-001-refactor-remove-all-emoji-plan.md b/docs/plans/2026-07-02-001-refactor-remove-all-emoji-plan.md new file mode 100644 index 0000000..b701cce --- /dev/null +++ b/docs/plans/2026-07-02-001-refactor-remove-all-emoji-plan.md @@ -0,0 +1,405 @@ +--- +title: "refactor: Remove all emoji from agentkit" +date: 2026-07-02 +type: refactor +status: approved +approved_at: 2026-07-02 +origin: user-direct (ce-plan invocation, 2026-07-02) +--- + +# refactor: Remove all emoji from agentkit + +## Summary + +`@board` 私董会数据恢复的修复已就位。但项目长期混用 emoji(专家头像、CLI 状态、私董会横幅、bitable 默认图标等),与既有的 Ant Design Vue Outlined 图标家族视觉风格不一致,且跨字体/OS 渲染不稳定。本次重构一次性把全部 emoji 收敛到三套等价替代: + +- **头像/单字符位** → 统一走 `expertIdentity.ts` 风格的「CJK/ASCII 首字符大写」;YAML/DB 中仍是字符串字段,前端渲染照常。 +- **横幅/卡片图标** → 改用 Ant Design Vue 组件(`BankOutlined`/`AuditOutlined`/`TeamOutlined` 等)。 +- **CLI 状态标记** → Rich 文本标签(`OK`/`FAIL`/`WARN`) + Rich 颜色样式。 + +执行顺序按 4 个批次分阶段落地,每批可独立提交/回滚。 + +## Problem Frame + +### 现状 + +全仓 22 个文件含 emoji 字符,覆盖 7 个语义类别(专家头像、bitable 默认、私董会横幅、辩论横幅、CLI 状态、终端提示、DB 默认值)。其中: + +- `configs/experts/*.yaml` 15 个文件使用 emoji 作为 avatar +- `bitable/db.py` 用 `📋` 作 `icon` 字段的 DB default(与 `models.py:80` 已有 `"table"` 默认不一致) +- `chat.py` 把 `🏛️ 私董会开始:…` 写入 `SqliteConversationStore`,作为无障碍/纯文本回退 +- `BoardBannerCard.vue`、`DebateBannerCard.vue`、`DebateConclusionCard.vue`、`UserBubble.vue` 直接渲染 emoji +- 4 个 CLI 文件用 `✓` `✗` `⚠` 作状态标记 +- 3 个测试文件把 emoji 作为 fixture + +### 触发原因 + +- 跨平台渲染不稳定:Linux 服务器无 Noto Color Emoji 时私董会横幅显示豆腐方块 +- 视觉风格不一致:与项目其余 Ant Design Vue Outlined 图标家族(1.5px stroke)不协调 +- DB schema 漂移:`bitable/models.py:80` 已迁到稳定 key `"table"`,但 `bitable/db.py:68,216` ORM/SQL 仍用 `📋` + +### 期望效果 + +- 全部 emoji 字符从仓库代码中清除(注释、文档除外) +- 视觉风格统一为 Ant Design Vue Outlined + 字符首字母 +- 旧 Bitable 行的 emoji 值通过前端 `resolveBitableIcon` 惰性回退到 `TableOutlined`,无需迁移 +- CLI 输出在所有终端(包括无 emoji 字体)下稳定显示 + +## Key Technical Decisions + +### KTD1:专家头像改为「首字符大写」策略,与 `expertIdentity.ts` 行为对齐 + +**决策**:YAML 中 `avatar` 字段从 emoji 改为对应专家名的首字符大写(中文取首个 CJK 字,英文取首字母大写)。保留 `IBoardExpert.avatar` 字段(不删),值由后端 `ExpertConfig` 解析或前端 `pickExpertIdentity` 兜底。 + +**理由**: +- `frontend/src/components/chat/helpers/expertIdentity.ts:55` 的 `pickExpertIdentity` 默认就是首字符策略 +- 删除字段会破坏既有 WebSocket payload(`board_started.experts[].avatar`)契约 +- 字符位渲染跨字体稳定 + +**取舍**:放弃了 emoji 头像的「人格化表达」,换取视觉一致性与跨平台稳定。Lead/主持人仍可通过 `color` token + tag 区分。 + +### KTD2:横幅/卡片图标改用 Ant Design Vue 组件,shell.avatar 改为 `Component` 类型 + +**决策**:`BoardBannerCard` / `DebateBannerCard` / `DebateConclusionCard` / `UserBubble` / `useMessageRenderer.ts` 引入 Ant Design Vue 组件(如 ``/``/``/``),把 shell.avatar 字段从 `string` 改为 `Component` 类型。 + +**理由**: +- 项目图标家族已统一为 Outlined(`Sidebar`、`Tabs`、`Tool call cards`) +- `useMessageRenderer.ts` 现有 `shell.avatar` 字段在 `MessageShell.vue` 渲染,与后端 `expert_avatar` 字段不冲突(那是单个专家的 avatar) + +**影响**:`MessageShell.vue` 接收 shell 的 avatar 字段如果是 `Component` 类型则用 ``;如果是 `string`(首字母)则原样渲染。 + +### KTD3:CLI 状态标记用 Rich 文本标签 + 颜色,弃用 `✓` `✗` `⚠` + +**决策**:`OK` / `FAIL` / `WARN` 作为文本标签,配合 Rich `[green]` / `[red]` / `[yellow]` 颜色。横幅标题用 `[OK 验收结果]` / `[WARN 风险标记]` 形式保留 emoji 缺失的视觉强度。 + +**理由**: +- 终端字体差异:Windows Terminal / 容器 tty / 旧版 macOS Terminal 对 Unicode 符号渲染不一致 +- 颜色已能传达同样信息量 +- 与 Python `rich` 库的最佳实践一致(标签 + 颜色) + +### KTD4:Bitable DB 默认值从 `📋` 改为 `table`,旧数据惰性收敛 + +**决策**: +- `bitable/db.py:68` ORM `default="📋"` → `default="table"` +- `bitable/db.py:216` SQL `DEFAULT '📋'` → `DEFAULT 'table'` +- **不写迁移脚本** — 前端 `resolveBitableIcon` 已对未知值回退到 `TableOutlined`,下次访问旧行时自动收敛 +- 新建行直接用 `table`;旧行 render 时也是 `TableOutlined`,零差异 + +**理由**: +- `bitable/models.py:80`(Pydantic)已经是 `"table"`,ORM 端对齐消除双默认值 +- 避免引入 Alembic 迁移;旧数据下次访问收敛 +- `bitable.ts:73` 和 `bitableIcons.ts:86` 已说明「legacy emoji 字符串自动回退」 + +**已知限制**:旧行在 DB 中仍是 `📋` 字符串。若未来要做 DB 备份快照或导出/导入,可能需要清理脚本。本次不处理。 + +### KTD5:App.vue 字体回退去掉 emoji 字体声明 + +**决策**:`App.vue:109-110` 中的 `'Apple Color Emoji'`、`'Segoe UI Emoji'`、`'Segoe UI Symbol'`、`'Noto Sans Emoji'` 字体回退删除。 + +**理由**:既然不再渲染任何 emoji 字符,浏览器/系统不会下载这些字体;删去减少 30KB+ 的潜在字体下载请求。保留 `sans-serif` 作为最终回退。 + +### KTD6:测试 fixture 同步更新,保持断言真实 + +**决策**:3 个测试文件中的 emoji fixture(`🤖` `🎯` `🐱` `💡` `🦊` `🐼` `🏛️`)改为对应的首字母大写(A/T/C/I/F/P 等),断言中的 `expect(avatars[0].textContent).toBe('🤖')` 同步改为 `'A'`。 + +**理由**:`restoreBoardStateFromMessages` 的 7 个新单元测试已锁定「缺 avatar/缺 color 走 fallback」行为,fixture 改字母不会绕过 fallback 链(仍由 `pickExpertIdentity(name)` 决定)。 + +## Scope Boundaries + +### 包含 + +- 22 个源码文件中的 emoji 字符移除(不含文档注释) +- 4 批次的实施顺序与每批对应的文件清单 +- DB 默认值从 emoji 改为稳定 key(无 schema migration) +- App.vue 字体回退清理 +- 3 个测试文件 fixture 同步 +- 验证:typecheck / pytest / vitest / 视觉冒烟 + +### 不包含(Deferred to Follow-Up Work) + +- **emoji 自动检测**(CI 守卫 / pre-commit 钩子)— 防止新增 emoji 复发,作为后续独立计划 +- **Bitable 旧行清理脚本**(DB 备份/导出场景下需要时再做) +- **`@ant-design/icons-vue` 包增量依赖** — 该包已是项目依赖(`bitableIcons.ts` 现有 import),无新增 +- **CLI 国际化(i18n)** — `OK`/`FAIL`/`WARN` 目前硬编码中英混排;国际化是更大范围重构,本次不动 +- **Tauri 桌面端 emoji 字体** — Tauri 客户端的 emoji 字体回退不在本计划范围 +- **avatar 字段语义升级**(如改成 `avatar_url` 支持真实头像图片)— 是产品级决策,超出本次范围 + +## System-Wide Impact + +| 受影响方 | 影响 | +| --- | --- | +| **终端用户(Web GUI)** | 私董会/辩论横幅、专家头像、Bitable 图标视觉变化;语义不变 | +| **终端用户(CLI)** | `agentkit` CLI 表格/状态标记从符号改为文本标签;颜色保留 | +| **终端用户(Tauri 桌面)** | 间接通过 Web GUI 同步 | +| **开发者** | 写新代码时不能直接用 emoji 字符;后续建议加 lint 规则 | +| **运营/部署** | 无(不涉及端口、配置、依赖增减) | +| **测试** | 3 个 vitest fixture 文件需同步更新断言;不影响测试覆盖度 | + +## High-Level Technical Design + +视觉风格统一(KTD1+KTD2)的目标是把混用状态收敛成两条规则: + +``` +┌─────────────────────────────────────────────────────┐ +│ 头像/单字符位 │ +│ • YAML avatar 字段 → "S"/"B"/"P" 等首字符 │ +│ • 前端 expertIdentity.ts 兜底(如缺 avatar) │ +│ • ExpertMessage/StickyModeHeader/BoardBannerCard │ +│ 都按 string 渲染字符 │ +└─────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────┐ +│ 横幅/卡片图标 │ +│ • BoardBannerCard: │ +│ • DebateBannerCard: │ +│ • DebateConclusionCard: 4 个决策图标 = 4 个组件 │ +│ • UserBubble command card: 私董会 │ +│ 团 │ +│ • useMessageRenderer shell.avatar 改为 Component │ +│ MessageShell 用 渲染 │ +└─────────────────────────────────────────────────────┘ +``` + +CLI 状态机收敛(KTD3): + +``` +旧: 新: +"✓" "✗" "⚠" "OK" "FAIL" "WARN" +[green]✓ X[/green] [bold green]OK X[/bold green] +[red]✗ X[/red] [bold red]FAIL X[/bold red] +[yellow]⚠ X[/yellow] [bold yellow]WARN X[/bold yellow] +``` + +DB schema 收敛(KTD4): + +``` +bitable/db.py:68 icon = Column(String, default="table") +bitable/db.py:216 icon VARCHAR DEFAULT 'table' + ↓ +旧 DB 行: '📋' → 前端 resolveBitableIcon 命中 DEFAULT_BITABLE_ICON='table' → 渲染 TableOutlined +新 DB 行: 'table' → 渲染 TableOutlined (与旧行一致) +``` + +## Implementation Units + +### U1. 数据/契约层 — avatar 字段、持久化文本、DB 默认值 + +**Goal**:把数据层/契约层的 emoji 全部收敛到字符串默认值或首字符。 + +**Files**: +- `configs/experts/*.yaml`(15 个文件:apple/private_board/team/tech_lead 等)— avatar 字段从 emoji 改首字符大写 +- `src/agentkit/bitable/db.py:68` — ORM default `📋` → `table` +- `src/agentkit/bitable/db.py:216` — SQL `DEFAULT '📋'` → `DEFAULT 'table'` +- `src/agentkit/server/routes/chat.py:304,306` — `🏛️ 私董会开始:…` → `私董会开始:…` +- `src/agentkit/server/frontend/src/stores/chatStream.ts:1149` — `🏛️ 私董会开始:…` → `私董会开始:…` +- `src/agentkit/core/plan_schema.py:107,122` — `📋`/`⚠️` → `[Plan ...]`/`WARN` +- `src/agentkit/experts/registry.py:85` — 注释 `📊` 改为纯文字 + +**Approach**: +- YAML avatar 字段值:英文名取首字母大写(`steve_jobs` → `S`),中文名取首 CJK 字(`张三` → `张`) +- 后端 `chat.py` 与前端 `chatStream.ts` 的 `board_started` 持久化文本保持语义一致(去 emoji 字符本身不影响 message_type/board_started metadata) +- `plan_schema.py:to_readable()` 是给人工看的报告,文本用纯文字更稳 + +**Test scenarios**: +- **Happy path**:`configs/experts/steve_jobs.yaml` 加载后 `expert.avatar == "S"` +- **Happy path**:`FileModel` 新实例 `icon == "table"` +- **Happy path**:`_apply_v1_schema` 生成的表 `icon` 列默认值为 `'table'` +- **Edge case**:现有 emoji DB 行(如 `icon='📋'`)通过 `resolveBitableIcon` 仍渲染为 `TableOutlined` + +**Verification**: +- `pytest tests/unit/experts -k "config or registry"` 通过 +- `pytest tests/unit/bitable -k "schema or model"` 通过 +- 启动 backend 调用 `@board`,确认 DB 中 `board_started` 消息 content 为 `私董会开始:...`(无 emoji) + +--- + +### U2. 前端 UI 组件 — 横幅、卡片、命令 chip + +**Goal**:把 emoji 图标改用 Ant Design Vue 组件。 + +**Files**: +- `src/agentkit/server/frontend/src/components/chat/messages/BoardBannerCard.vue` — `🏛️` → `` +- `src/agentkit/server/frontend/src/components/chat/messages/DebateBannerCard.vue` — `⚖` → `` +- `src/agentkit/server/frontend/src/components/chat/messages/DebateConclusionCard.vue` — `decisionIcons` map 改为 Ant Design Vue 组件(`CheckOutlined`/`SwapOutlined`/`MinusOutlined`/`QuestionOutlined`) +- `src/agentkit/server/frontend/src/components/chat/messages/UserBubble.vue:143` — `'🏛️'`/`'👥'` 字符串 → ``/`` +- `src/agentkit/server/frontend/src/components/chat/helpers/useMessageRenderer.ts:212,271` — `avatar: '⚖'` → `avatar: AuditOutlined`(shell.avatar 改 Component) +- `src/agentkit/server/frontend/src/components/chat/MessageShell.vue` — 检查 avatar 字段类型,Component 走 ``,string 走原样 +- `src/agentkit/server/frontend/src/App.vue:109-110` — 字体回退删 emoji 字体声明 + +**Approach**: +- 引入 `BankOutlined` / `AuditOutlined` / `TeamOutlined` / `CheckOutlined` / `SwapOutlined` / `MinusOutlined` / `QuestionOutlined`(均已在 `@ant-design/icons-vue` 中,无需新装) +- `BoardBannerCard` / `DebateBannerCard` 的 `X` 改为 `` +- `UserBubble` 的 command card 改造:`icon` prop 改为 `Component` 类型 +- `useMessageRenderer` 中 `shell.avatar` 类型由 `string` 改为 `Component`(仅这两处),其余 shell 仍用 string(首字母) +- `MessageShell.vue` 用 `v-if="typeof shell.avatar === 'string'"` 分支处理 + +**Test scenarios**: +- **Happy path**:`BoardBannerCard` 渲染时包含 `BankOutlined` SVG path +- **Happy path**:`DebateBannerCard` 渲染时包含 `AuditOutlined` SVG path +- **Happy path**:`DebateConclusionCard decision="adopt"` 渲染 `CheckOutlined` +- **Happy path**:`UserBubble` 解析 `@board:alice, bob` 时 command icon 为 `BankOutlined` +- **Edge case**:`shell.avatar` 是 string(如 `expert_initial`)时仍原样渲染,不被当作 component + +**Verification**: +- `npm run typecheck` 通过 +- `npx vitest run tests/unit/components/` 通过 +- 浏览器打开 `/agent/chat` 触发 `@board`,确认横幅显示 `` 矢量图标(非 emoji) +- 触发辩论确认 `DebateBannerCard` 显示 `` + +--- + +### U3. CLI 控制台输出 — admin / chat / skill / benchmark + +**Goal**:把 Rich 输出中的 `✓` `✗` `⚠` 改为 `OK`/`FAIL`/`WARN` 文本标签 + Rich 颜色。 + +**Files**: +- `src/agentkit/cli/admin.py:215,601` — 表格 `is_active` 列的 `✓`/`✗` → `OK`/`--` +- `src/agentkit/cli/chat.py:589,603,692,694,727,730` — 验收/阶段/风险标记全部收敛 +- `src/agentkit/cli/skill.py:296` — `⚠ 以下为自动生成…` → `WARN 以下为自动生成…` +- `src/agentkit/cli/benchmark.py:895,989,2032,2733,2767,2784,2790` — 所有 `✓`/`✗`/`⚠` 收敛 + +**Approach**: +- 字符串替换策略:所有 `[green]✓ X[/green]` 改 `[bold green]OK X[/bold green]`;`[red]✗ X[/red]` 改 `[bold red]FAIL X[/red]`;`[yellow]⚠ X[/yellow]` 改 `[bold yellow]WARN X[/yellow]` +- 表格列中的 `✓`/`✗` 用 `OK`/`--`(失败用 `--` 而非 `FAIL` 因为列宽有限) +- 横幅标题的 `⚠ 风险标记` 改 `WARN 风险标记`(保留全部大写作为视觉强度替代) + +**Test scenarios**: +- **Happy path**:`agentkit admin list-users` 输出包含 `OK` 而非 `✓` +- **Happy path**:`agentkit chat` 触发的 phase completion 打印 `[OK] 阶段名:summary` +- **Error path**:phase 失败时打印 `[FAIL] 阶段名:error` +- **Integration**:`agentkit benchmark` 在失败用例上打印 `[FAIL]`(无 emoji) + +**Verification**: +- `pytest tests/unit/cli -k "admin or chat or skill or benchmark"` 通过 +- 手动跑 `agentkit admin list-users` / `agentkit chat "test"` / `agentkit benchmark list` 确认输出无 emoji + +--- + +### U4. 终端前端 + 测试 fixture + 字体回退 + +**Goal**:清理剩余的终端提示、测试 fixture、字体回退。 + +**Files**: +- `src/agentkit/server/frontend/src/stores/terminal.ts:251,262,266,270,275` — `⚠` `⏳` `✓` `✗` → `WARN` `PENDING` `OK` `REJECTED`/`TIMEOUT` +- `src/agentkit/server/frontend/src/components/terminal/CommandHistory.vue:20` — `✓`/`✗` → `OK`/`FAIL` +- `src/agentkit/server/frontend/tests/unit/stores/chatStream.test.ts:731` — `avatar: '🦊'` → `avatar: 'F'` +- `src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts:22` — `content: '🏛️ 私董会开始:…'` → `content: '私董会开始:…'` +- `src/agentkit/server/frontend/tests/unit/stores/chatStore.test.ts:33,40` — `avatar: '🦊'`/`'🐼'` → `avatar: 'F'`/`'P'` +- `src/agentkit/server/frontend/tests/unit/components/StickyModeHeader.test.ts` — 所有 emoji fixture 改首字母 +- `src/agentkit/server/frontend/src/components/chat/messages/TeamPlanCard.vue:67,82,127` — 注释更新(已说明弃用,保留为历史说明即可,无运行影响) + +**Approach**: +- 终端消息用 `[WARN]` / `[PENDING]` / `[OK]` / `[REJECTED]` / `[TIMEOUT]` 文本格式(与 CLI KTD3 保持一致) +- 测试 fixture 改为对应名称首字母(`StickyModeHeader.test.ts` 中 `avatar: '🤖'` → `avatar: 'A'`,`expect(avatars[0].textContent).toBe('A')`) +- `chatStore.test.ts` 的 `board_started` content 改 `私董会开始:AI 未来`(与 U1 后端持久化文本一致) + +**Test scenarios**: +- **Happy path**:`CommandHistory` 渲染 exit_code=0 时显示 `OK`、非 0 时显示 `FAIL` +- **Happy path**:`StickyModeHeader.test.ts` 中 `expect(avatars[0].textContent).toBe('A')` 通过 +- **Happy path**:`chatStore.test.ts` `restoreBoardStateFromMessages` 测试对新的非-emoji content 仍正确解析 +- **Edge case**:终端 store 中 `appendOutput` 的 ANSI 颜色(`\x1b[33m`)保留,仅替换前缀文本 + +**Verification**: +- `npm run typecheck` 通过 +- `npx vitest run tests/unit/` 全通过 +- 终端面板审批流程人工冒烟 + +--- + +### U5. 端到端验证 + 防止复发(lint 规则 + 文档) + +**Goal**:确认全仓无残留 emoji,并加入 CI 守卫。 + +**Files**: +- 新增 `frontend/eslint-rules/no-emoji.cjs`(自定义 ESLint 规则,禁止源码中的 emoji 字符范围) +- `frontend/.eslintrc.cjs` 注册规则 +- `pyproject.toml` 加 `ruff` 自定义规则(`RUF900` 禁用 emoji)或依赖 `pre-commit` 钩子 +- `.pre-commit-config.yaml`(若不存在则创建)— 添加 `agentkit-no-emoji` 钩子 +- `docs/solutions/style/no-emoji-style-guide.md`(新增)— 记录替换规则与原因 + +**Approach**: +- 自定义 ESLint 规则用正则 `[\x{1F000}-\x{1FFFF}\x{2600}-\x{27BF}\x{2300}-\x{23FF}\x{2B00}-\x{2BFF}]` 匹配所有 emoji 范围 +- 规则名 `no-emoji-in-source`,错误级别 `error`,白名单文件:注释(通过 `// allow-emoji` 注解豁免,仅在文档/迁移脚本等场景) +- ruff 通过 `RUF` 自定义检查或第三方 `flake8-no-emoji` 工具 +- pre-commit 在 commit 阶段跑前端 + 后端双检查 +- 风格指南文档说明三套替代策略(KTD1-3) + +**Test scenarios**: +- **Happy path**:`npx eslint src/` 无 emoji 错误 +- **Happy path**:`ruff check src/` 无 emoji 警告 +- **Error path**:故意在某 `.vue` 文件中加一个 emoji 字符,ESLint 报错并阻止 build +- **Integration**:本地 commit 含 emoji 的文件被 pre-commit 阻止 + +**Verification**: +- `npm run lint` 全通过 +- `ruff check src/` 全通过 +- `pre-commit run --all-files` 全通过 +- 新建一个临时 `.vue` 文件含 emoji,确认 ESLint 报错 + +**Deferred to Follow-Up**:CI 工作流(`.github/workflows/` 或 Gitea Actions)添加 emoji 检查步骤 — 等 lint 规则稳定后再加,避免一次性改动过多。 + +--- + +## Risks & Dependencies + +| 风险 | 缓解 | +| --- | --- | +| Ant Design Vue 图标视觉与原 emoji 风格差异大,用户不适应 | U2 完成后做视觉冒烟;如不接受可回退到 `string` 字符首字母策略(KTD1) | +| `useMessageRenderer.ts` 的 `shell.avatar` 改 `Component` 类型影响 `MessageShell.vue` 之外的消费者 | 全仓 grep `shell.avatar` / `message.shell.avatar` 确认仅 `MessageShell.vue` 消费 | +| CLI 输出改为 `OK`/`FAIL` 文本会改变宽度,可能破坏现有对齐 | 表格用 `--` 而非 `FAIL`(列宽更短) | +| Bitable 旧行不清理,未来 DB 导出/快照会有 `📋` 字符串 | 文档记录 KTD4 限制;后续可加迁移脚本 | +| pre-commit / 自定义 ESLint 规则本身可能误报(CJK 字符、注释中的 emoji) | 规则用明确的 Unicode range;白名单机制支持 `// allow-emoji` 行级豁免 | +| 测试 fixture 改首字母可能与某个测试断言的「fallback 行为」冲突 | U4 前先跑一遍 vitest 全量测试,建立基线;逐个 fixture 改并立即跑对应测试 | + +**依赖关系**: +- U1 独立(数据/契约层) +- U2 独立(前端 UI) +- U3 独立(CLI) +- U4 依赖 U1(chatStore.test.ts content 需与 U1 后端持久化文本一致) +- U5 依赖 U1-U4 全部完成 + +**实施顺序**:U1 → U2 → U3 → U4 → U5。每批独立可提交/可回滚。 + +## Acceptance Examples + +AE1. **私董会横幅显示矢量图标** — 触发 `@board 测试主题`,确认 `BoardBannerCard` 渲染 `` 矢量图标(SVG),不再显示 `🏛️` 字符。 + +AE2. **Bitable 默认图标稳定** — 创建新 Bitable 文件,`icon` 字段在 DB 中为 `'table'`,UI 渲染 `TableOutlined`。已存在的 emoji 行通过前端 `resolveBitableIcon` 自动收敛。 + +AE3. **CLI 状态无 emoji** — 运行 `agentkit admin list-users`,表格中 `is_active=true` 显示 `OK`,`is_active=false` 显示 `--`,无 `✓`/`✗` 字符。 + +AE4. **专家头像首字符** — `@board steve_jobs, charlie_munger` 私董会,`BoardBannerCard` 中 Steve Jobs 头像显示 `S`、Charlie Munger 头像显示 `C`(不再显示 `🍎`/`🧠`)。 + +AE5. **测试 fixture 与生产代码一致** — `npm run typecheck` + `npx vitest run` + `pytest tests/unit/` 三套全通过,无 emoji 残留。 + +AE6. **CI 守卫** — 故意在某 `.vue` 模板加 `🏛️` 字符,`npm run lint` 报错并阻止。 + +## Open Questions + +1. **`useMessageRenderer.ts` 的 `shell.avatar` 类型升级是否需要更平滑**(保留 string 但用约定 `__ICON__BankOutlined`)?— 当前方案改为 `Component` 是更类型安全的,但若 `MessageShell.vue` 之外有第三方消费方可能破坏。**默认决定**:U2 实施前先全仓 grep 确认无外部消费者。 + +2. **Bitable `📋` 旧行是否要做一次性清理脚本**?— KTD4 决定惰性收敛,文档记录为 follow-up。如用户要求显式清理,U5 后可加一个 `agentkit bitable migrate-icons` 命令。 + +3. **`@ant-design/icons-vue` 包体积**(目前约 300KB+)— 项目已在用,仅新增 7 个组件,bundle size 影响可忽略。如未来有更激进的优化诉求可走按需 import + Vite tree-shaking(已默认开启)。 + +## Sources & Research + +- **本地参考**: + - `frontend/src/components/chat/helpers/expertIdentity.ts` — 字符首字母策略的权威实现 + - `frontend/src/components/bitable/bitableIcons.ts:89` — `resolveBitableIcon` 的回退路径 + - `docs/plans/2026-06-19-001-feat-chat-area-vi-redesign-plan.md:192` — BoardBannerCard 的设计说明(含 `🏛️`) + - `docs/plans/2026-07-01-001-feat-ui-ue-enhancement-plan.md:154-155` — StickyModeHeader 的 emoji 头像渲染说明 +- **项目记忆**: + - 「UI components must use consistent color tokens; avoid hardcoded blue fallback colors」— 沿用到图标风格统一 + - 「CSS token fallback values can cause unintended color changes when tokens load late」— Ant Design Vue 组件是设计良好的回退机制 +- **无外部研究触发**:emoji 替换是纯内部风格收敛,无外部 API/技术依赖。 + +--- + +## Plan Metadata + +- **Depth**: Standard(5 个 implementation units,跨前后端+CLI+DB+测试+lint 守卫) +- **Expected commits**: 5(每个 U 一个 commit;U5 包含 lint 规则可能拆 2 个) +- **Risk profile**: 中(无安全/支付/外部 API 风险,主要是视觉/UX 风险) +- **Origin**: 用户直接在 ce-plan 中提出(无 upstream brainstorm doc) +- **Confidence**: 中高(KTD1-4 都有项目内既有模式可遵循;KTD5/6 引入新机制,U5 留验证空间) diff --git a/src/agentkit/bitable/models.py b/src/agentkit/bitable/models.py index db8d90e..c5e7d21 100644 --- a/src/agentkit/bitable/models.py +++ b/src/agentkit/bitable/models.py @@ -74,7 +74,10 @@ class BitableFile(BaseModel): id: str name: str - icon: str = "📋" + # Stable icon key (e.g. ``"table"``); resolved client-side through + # ``bitableIcons.ts``. See that module for the full key set and the + # emoji-string fallback for legacy rows. + icon: str = "table" description: str = "" owner_user_id: str | None = None created_at: datetime = PydanticField(default_factory=_utcnow) diff --git a/src/agentkit/bitable/repository.py b/src/agentkit/bitable/repository.py index 8278ff5..51892fb 100644 --- a/src/agentkit/bitable/repository.py +++ b/src/agentkit/bitable/repository.py @@ -61,7 +61,7 @@ class BitableRepository: async def create_file( self, name: str, - icon: str = "📋", + icon: str = "table", description: str = "", owner_user_id: str | None = None, ) -> BitableFile: diff --git a/src/agentkit/bitable/service.py b/src/agentkit/bitable/service.py index 84ab40e..bbe83ad 100644 --- a/src/agentkit/bitable/service.py +++ b/src/agentkit/bitable/service.py @@ -91,7 +91,7 @@ class BitableService: async def create_file( self, name: str, - icon: str = "📋", + icon: str = "table", description: str = "", owner_user_id: str | None = None, ) -> BitableFile: diff --git a/src/agentkit/chat/sqlite_conversation_store.py b/src/agentkit/chat/sqlite_conversation_store.py index 749be29..827ea72 100644 --- a/src/agentkit/chat/sqlite_conversation_store.py +++ b/src/agentkit/chat/sqlite_conversation_store.py @@ -368,3 +368,42 @@ class SqliteConversationStore: except (aiosqlite.Error, ValueError, KeyError, TypeError, RuntimeError) as e: logger.warning(f"get_first_user_message failed for {conversation_id}: {e}") return None + + async def has_message_with_type( + self, conversation_id: str, message_type: str + ) -> bool: + """Return True if the conversation has at least one message whose + ``metadata.message_type`` equals *message_type*. + + Cheap existence check used by the sidebar to flag board meetings + (``"board_started"``) without fetching the full history. Backs + the per-conversation ``is_board`` flag returned by + ``/api/v1/portal/conversations``. + + The metadata column is JSON, so we LIKE-search for the literal + ``"message_type": ""`` token. SQLite has no native JSON + functions enabled in the schema, and ``json_extract`` would + require the JSON1 extension — LIKE keeps the query portable and + cheap (uses the existing ``idx_messages_conv_id`` index for the + ``conversation_id`` predicate). + """ + db = await self._ensure_db() + # Escape backslashes/quotes defensively even though ``message_type`` + # values are currently hard-coded identifiers; future callers might + # pass user input. + safe = message_type.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + like_pattern = f'%"message_type": "{safe}"%' + try: + cursor = await db.execute( + "SELECT 1 FROM messages " + "WHERE conversation_id = ? AND metadata LIKE ? ESCAPE '\\' " + "LIMIT 1", + (conversation_id, like_pattern), + ) + row = await cursor.fetchone() + return row is not None + except (aiosqlite.Error, ValueError, TypeError) as e: + logger.warning( + f"has_message_with_type failed for {conversation_id}/{message_type}: {e}" + ) + return False diff --git a/src/agentkit/cli/admin.py b/src/agentkit/cli/admin.py index f16393a..9a4ab31 100644 --- a/src/agentkit/cli/admin.py +++ b/src/agentkit/cli/admin.py @@ -36,7 +36,7 @@ console = Console() # --------------------------------------------------------------------------- ServerUrlOption = typer.Option( - None, "--server-url", "-s", help="Server URL (default: http://localhost:8001)" + None, "--server-url", "-s", help="Server URL (default: http://localhost:18001)" ) TokenOption = typer.Option(None, "--token", "-t", help="JWT access token") ApiKeyOption = typer.Option(None, "--api-key", "-k", help="API key") @@ -161,7 +161,7 @@ def admin_login( ), ) -> None: """Login and save the access token to ``~/.agentkit/admin_config.yaml``.""" - resolved_url = server_url or "http://localhost:8001" + resolved_url = server_url or "http://localhost:18001" client = AdminHttpClient(resolved_url) try: token = client.login(username, password) diff --git a/src/agentkit/cli/admin_client.py b/src/agentkit/cli/admin_client.py index c0bb805..1f1b1ae 100644 --- a/src/agentkit/cli/admin_client.py +++ b/src/agentkit/cli/admin_client.py @@ -14,7 +14,7 @@ from pathlib import Path import httpx import yaml -DEFAULT_SERVER_URL = "http://localhost:8001" +DEFAULT_SERVER_URL = "http://localhost:18001" DEFAULT_CONFIG_PATH = Path.home() / ".agentkit" / "admin_config.yaml" DEFAULT_TIMEOUT = 30.0 diff --git a/src/agentkit/cli/init.py b/src/agentkit/cli/init.py index b6b456e..e59453a 100644 --- a/src/agentkit/cli/init.py +++ b/src/agentkit/cli/init.py @@ -51,4 +51,4 @@ def init( rprint(" 1. Copy [cyan].env.example[/cyan] to [cyan].env[/cyan] and fill in your API keys") rprint(" 2. Edit [cyan]agentkit.yaml[/cyan] to configure your agents") rprint(" 3. Run [cyan]agentkit serve[/cyan] to start the server") - rprint(" 4. Run [cyan]agentkit task submit --skill example_skill --input '{\"message\": \"Hello\"}' --server-url http://localhost:8001[/cyan]") + rprint(" 4. Run [cyan]agentkit task submit --skill example_skill --input '{\"message\": \"Hello\"}' --server-url http://localhost:18001[/cyan]") diff --git a/src/agentkit/cli/main.py b/src/agentkit/cli/main.py index c0aa8da..1662e02 100644 --- a/src/agentkit/cli/main.py +++ b/src/agentkit/cli/main.py @@ -51,7 +51,7 @@ app.command(name="benchmark")(benchmark) @app.command() def gui( host: str = typer.Option("0.0.0.0", "--host", help="Server bind host"), - port: int = typer.Option(8002, "--port", help="Server port (0 for random)"), + port: int = typer.Option(18002, "--port", help="Server port (0 for random)"), config: Optional[str] = typer.Option(None, "--config", help="Path to agentkit.yaml"), no_open: bool = typer.Option(False, "--no-open", help="Do not open browser automatically"), ): @@ -166,7 +166,7 @@ def gui( @app.command() def serve( host: str = typer.Option("0.0.0.0", "--host", help="Server host"), - port: int = typer.Option(8001, "--port", help="Server port"), + port: int = typer.Option(18001, "--port", help="Server port"), workers: int = typer.Option(1, "--workers", help="Number of workers"), reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"), config: Optional[str] = typer.Option(None, "--config", help="Path to agentkit.yaml"), @@ -228,7 +228,7 @@ def serve( # CLI args override config file effective_host = host if host != "0.0.0.0" else server_config.host - effective_port = port if port != 8001 else server_config.port + effective_port = port if port != 18001 else server_config.port effective_workers = workers if workers != 1 else server_config.workers # Store config for app factory @@ -288,7 +288,7 @@ def version(): @app.command() def doctor( host: str = typer.Option("localhost", "--host", help="Server host"), - port: int = typer.Option(8001, "--port", help="Server port"), + port: int = typer.Option(18001, "--port", help="Server port"), ): """Diagnose AgentKit server health and configuration""" import httpx diff --git a/src/agentkit/cli/onboarding.py b/src/agentkit/cli/onboarding.py index f274ad8..ce5bd99 100644 --- a/src/agentkit/cli/onboarding.py +++ b/src/agentkit/cli/onboarding.py @@ -290,7 +290,7 @@ def run_onboarding( config = { "server": { "host": "0.0.0.0", - "port": 8001, + "port": 18001, "workers": 1, "rate_limit": 60, }, diff --git a/src/agentkit/cli/pair.py b/src/agentkit/cli/pair.py index fa948ce..0564c59 100644 --- a/src/agentkit/cli/pair.py +++ b/src/agentkit/cli/pair.py @@ -39,7 +39,7 @@ def pair( config_dir: str = typer.Option(".", "--config-dir", help="AgentKit config directory"), list_clients: bool = typer.Option(False, "--list", "-l", help="List all paired clients"), revoke: Optional[str] = typer.Option(None, "--revoke", "-r", help="Revoke a client by name"), - server_url: str = typer.Option("http://localhost:8001", "--server-url", help="AgentKit server URL for connection instructions"), + server_url: str = typer.Option("http://localhost:18001", "--server-url", help="AgentKit server URL for connection instructions"), ): """Pair a business system with AgentKit (generate API key + register client)""" config_dir = os.path.abspath(config_dir) diff --git a/src/agentkit/cli/templates.py b/src/agentkit/cli/templates.py index 333d8c7..4d0b0ba 100644 --- a/src/agentkit/cli/templates.py +++ b/src/agentkit/cli/templates.py @@ -6,7 +6,7 @@ AGENTKIT_YAML = """\ server: host: "0.0.0.0" - port: 8001 + port: 18001 workers: 1 api_key: null # Set to enable API key authentication rate_limit: 60 # Requests per minute @@ -77,9 +77,9 @@ version: "3.8" services: agentkit: build: . - command: serve --host 0.0.0.0 --port 8001 + command: serve --host 0.0.0.0 --port 18001 ports: - - "8001:8001" + - "18001:18001" env_file: .env depends_on: redis: @@ -87,7 +87,7 @@ services: postgres: condition: service_healthy healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8001/api/v1/health')"] + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:18001/api/v1/health')"] interval: 30s timeout: 10s retries: 3 diff --git a/src/agentkit/client/sync.py b/src/agentkit/client/sync.py index 8f5710a..4540df8 100644 --- a/src/agentkit/client/sync.py +++ b/src/agentkit/client/sync.py @@ -9,7 +9,7 @@ Usage:: from agentkit.client.sync import ConfigSync sync = ConfigSync( - server_url="http://localhost:8001", + server_url="http://localhost:18001", token_provider=lambda: jwt_token, # or None for dev mode cache_db_path="~/.agentkit/config_cache.db", ) @@ -78,7 +78,7 @@ class ConfigSync: changes on a configurable interval. Attributes: - server_url: Base URL of the AgentKit server (e.g. ``http://localhost:8001``). + server_url: Base URL of the AgentKit server (e.g. ``http://localhost:18001``). token_provider: Callable that returns the current JWT access token (or ``None`` if not authenticated). Called on each request. cache_db_path: Path to the local SQLite cache file. diff --git a/src/agentkit/experts/board_orchestrator.py b/src/agentkit/experts/board_orchestrator.py index d9d89e1..7b6ddb7 100644 --- a/src/agentkit/experts/board_orchestrator.py +++ b/src/agentkit/experts/board_orchestrator.py @@ -75,9 +75,7 @@ class BoardOrchestrator: # Promote first active expert to moderator self._team._moderator_name = active[0].config.name moderator = active[0] - logger.warning( - f"Moderator not available, falling back to '{moderator.config.name}'" - ) + logger.warning(f"Moderator not available, falling back to '{moderator.config.name}'") self._team.set_status(BoardStatus.DISCUSSING) @@ -128,22 +126,22 @@ class BoardOrchestrator: logger.info(f"Discussion stopped by user at round {round_num}") break - # Generate member speeches in parallel + # Generate member speeches sequentially so the user sees one + # expert at a time instead of all experts racing to print + # their full text in the same instant. Each expert streams + # its own chunks (via expert_speech_chunk events) before the + # next expert starts. This trades wall-clock latency for + # perceptual clarity, which matches the user's stated + # preference for "逐个输出". Ponytail: 1-N experts, simple + # for-loop; if board size grows past ~5 members, revisit. members = self._team.member_experts if members: - speech_results = await asyncio.gather( - *[self._generate_expert_speech(e, round_num) for e in members], - return_exceptions=True, - ) - - # Broadcast speeches in order (not parallel broadcast) - for expert, result in zip(members, speech_results): - if isinstance(result, Exception): - logger.warning( - f"Expert '{expert.config.name}' speech failed: {result}" - ) + for expert in members: + try: + result = await self._generate_expert_speech(expert, round_num) + except Exception as e: + logger.warning(f"Expert '{expert.config.name}' speech failed: {e}") continue - await self._team.add_to_history( round_num, expert.config.name, result, "expert" ) @@ -265,12 +263,17 @@ class BoardOrchestrator: return f"欢迎来到私董会。今天的讨论主题是:{topic}。请各位专家发表看法。" async def _generate_expert_speech(self, expert: Expert, round: int) -> str: - """Generate an expert's speech for the current round. + """Generate an expert's speech for the current round (streaming). The speech is based on: - Expert's persona, thinking_style, speaking_style, decision_framework - Full discussion history - Current round / max rounds + + Streams LLM output chunk-by-chunk via ``expert_speech_chunk`` events + so the UI shows content as it arrives, instead of waiting for the + full completion. Returns the accumulated content so the gather + step in ``execute()`` can still keep its parallel contract. """ gateway = self._get_llm_gateway(expert) if not gateway: @@ -300,11 +303,126 @@ class BoardOrchestrator: "- 给出明确的立场或建议\n" ) - response = await gateway.chat( - messages=[{"role": "user", "content": prompt}], - model="default", - ) - return response.content.strip() + # ponytail: stream when the provider supports it, otherwise fall back + # to a single non-streaming call. We keep the gather contract — the + # function still returns the full text — so the surrounding execute() + # loop is unchanged. + return await self._stream_expert_speech(expert, round, prompt) + + async def _stream_expert_speech(self, expert: Expert, round: int, prompt: str) -> str: + """Stream an expert's speech via chat_stream, emitting chunks. + + Falls back to non-streaming ``chat()`` when ``chat_stream`` is + unavailable (e.g. an LLM provider without streaming support) or + raises before any chunk is produced. + + ponytail: when the LLM does not actually stream (returns a single + big chunk), we still want the UI to see content appearing + progressively. So we split the LLM's final content into + sentence/line chunks and emit them with a small delay. The + ``expert_speech_chunk`` event already handles duplicate-sender + dedup, so emitting many small chunks is safe. + """ + gateway = self._get_llm_gateway(expert) + assert gateway is not None # checked by caller + total = "" + # Emit an opening chunk-less event so the UI can create the streaming + # placeholder before the first token arrives (keeps the first paint + # aligned with the streaming indicator). + try: + streamed_chunk_count = 0 + async for chunk in gateway.chat_stream( + messages=[{"role": "user", "content": prompt}], + model="default", + ): + delta = chunk.content or "" + if not delta: + continue + total += delta + streamed_chunk_count += 1 + await self._broadcast_event( + "expert_speech_chunk", + { + "expert_name": expert.config.name, + "expert_avatar": expert.config.avatar, + "expert_color": expert.config.color, + "content": delta, + "round": round, + "role": "expert", + }, + ) + # If the LLM "streamed" but only delivered one big chunk, still + # let the UI see content arrive progressively. + if streamed_chunk_count <= 1 and total: + await self._replay_stream(expert, round, total, delay=0.05, chunk_size=12) + return total.strip() + except (AttributeError, NotImplementedError) as e: + logger.info( + f"Provider for '{expert.config.name}' lacks chat_stream, " + f"falling back to non-streaming: {e}" + ) + except Exception as e: + logger.warning(f"Expert '{expert.config.name}' stream failed: {e}") + + # Fallback: non-streaming path. Emit the whole content as small + # chunks so the UI still renders progressively rather than going + # silent and then dumping the whole text in one frame. + try: + response = await gateway.chat( + messages=[{"role": "user", "content": prompt}], + model="default", + ) + content = (response.content or "").strip() + if content: + await self._replay_stream(expert, round, content, delay=0.05, chunk_size=12) + return content + except Exception as e: + logger.warning(f"Expert '{expert.config.name}' non-stream fallback failed: {e}") + return total.strip() or f"[{expert.config.name} 发言失败]" + + async def _replay_stream( + self, + expert: Expert, + round: int, + content: str, + *, + delay: float, + chunk_size: int, + ) -> None: + """Emit ``content`` as small ``expert_speech_chunk`` events. + + Used when the LLM provider returns the whole response in a single + chunk — the UI otherwise sees no streaming animation. Splits on + Chinese sentence boundaries (``。!?\n``) and falls back to a + fixed character count for safety. + """ + import re + + # Split on sentence/line boundaries, keeping the delimiters so the + # joined output still reads naturally. + parts = re.findall(r"[^。!?\n]+[。!?\n]?|[^。!?\n]+", content) + if not parts: + parts = [content] + for part in parts: + # If a part is huge (no delimiters hit), slice it into + # sub-chunks of ``chunk_size`` characters. + for start in range(0, len(part), chunk_size): + piece = part[start : start + chunk_size] + if not piece: + continue + await self._broadcast_event( + "expert_speech_chunk", + { + "expert_name": expert.config.name, + "expert_avatar": expert.config.avatar, + "expert_color": expert.config.color, + "content": piece, + "round": round, + "role": "expert", + }, + ) + if delay > 0: + await asyncio.sleep(delay) async def _generate_moderator_summary(self, moderator: Expert, round: int) -> str: """Generate moderator's round summary. @@ -316,15 +434,11 @@ class BoardOrchestrator: return f"[第 {round} 轮小结因 LLM 不可用无法生成]" # Get only current round's speeches - round_history = [ - h for h in self._team.history if h["round"] == round - ] + round_history = [h for h in self._team.history if h["round"] == round] if not round_history: return "" - round_text = "\n\n".join( - f"[{h['expert_name']}]: {h['content']}" for h in round_history - ) + round_text = "\n\n".join(f"[{h['expert_name']}]: {h['content']}" for h in round_history) prompt = ( f"你是私董会主持人 {moderator.config.name}。\n" @@ -427,7 +541,9 @@ class BoardOrchestrator: "dissent_points": [], } - async def _generate_fallback_conclusion(self, moderator: Expert, topic: str) -> dict[str, object]: + async def _generate_fallback_conclusion( + self, moderator: Expert, topic: str + ) -> dict[str, object]: """Generate a fallback conclusion when execution fails. Uses existing discussion history to provide a basic summary. diff --git a/src/agentkit/memory/adapters/generic_http.py b/src/agentkit/memory/adapters/generic_http.py index 8a9b5c3..843d966 100644 --- a/src/agentkit/memory/adapters/generic_http.py +++ b/src/agentkit/memory/adapters/generic_http.py @@ -32,7 +32,7 @@ class GenericHTTPAdapter(KBAdapter): 典型配置:: adapter = GenericHTTPAdapter( - endpoint_url="http://localhost:8000/api/knowledge", + endpoint_url="http://localhost:18001/api/knowledge", auth_config={"type": "bearer", "token": "sk-xxx"}, headers={"X-Custom-Header": "value"}, ) diff --git a/src/agentkit/memory/http_rag.py b/src/agentkit/memory/http_rag.py index af09bbf..e85c703 100644 --- a/src/agentkit/memory/http_rag.py +++ b/src/agentkit/memory/http_rag.py @@ -36,7 +36,7 @@ class HttpRAGService: memory: semantic: enabled: true - base_url: "http://localhost:8000/api/knowledge" + base_url: "http://localhost:18001/api/knowledge" api_key: "${GEO_API_KEY}" knowledge_base_ids: - "industry-kb-id" @@ -56,7 +56,7 @@ class HttpRAGService: ): """ Args: - base_url: 知识库 API 基础地址,如 http://localhost:8000/api/knowledge + base_url: 知识库 API 基础地址,如 http://localhost:18001/api/knowledge api_key: 认证 API Key(放在 Authorization: Bearer 头) knowledge_base_ids: 默认检索的知识库 ID 列表 timeout: HTTP 请求超时秒数 diff --git a/src/agentkit/server/app.py b/src/agentkit/server/app.py index 60373c8..9cb8d44 100644 --- a/src/agentkit/server/app.py +++ b/src/agentkit/server/app.py @@ -183,6 +183,7 @@ async def lifespan(app: FastAPI): from agentkit.tools.web_crawl import WebCrawlTool from agentkit.tools.baidu_search import BaiduSearchTool from agentkit.tools.document_tool import DocumentTool + from agentkit.tools.file_read import ReadFileTool # benchmark_runner skill binding from agentkit.documents.service import DocumentService from agentkit.documents.db import init_documents_db from agentkit.documents.renderers.word_renderer import WordRenderer @@ -262,6 +263,7 @@ async def lifespan(app: FastAPI): agent._tool_registry.register(BaiduSearchTool()) agent._tool_registry.register(WebSearchTool(**search_api_keys)) agent._tool_registry.register(WebCrawlTool()) + agent._tool_registry.register(ReadFileTool()) # benchmark_runner skill binding # Document processing tool (U6): DocumentService with all renderers. # On failure the tool is simply unavailable — app.state.document_service @@ -816,11 +818,22 @@ def create_app( config_path = str(_cwd_yaml) if config_path and os.path.exists(config_path): - # Load .env before parsing config (so ${ENV_VAR} substitutions work) + # Load .env before parsing config (so ${ENV_VAR} substitutions work). + # Try multiple candidates in priority order: ``.env`` (canonical, + # matches the rest of the Python ecosystem) → ``.env.dev`` (our + # in-tree dev preset for the 18001/15173/15174 port allocation) + # → ``.env.local`` (developer-local overrides, gitignored). + # + # ponytail: 改之前只查 .env,导致 .env.dev 不被加载 → Bitable + # 拿不到 DATABASE_URL,BitTable 初始化失败。三个候选按优先级叠加 + # 加载(不互相覆盖 os.environ),与 docker-compose 的 + # env_file priority 行为一致。升级路径:如果项目开始用 + # python-dotenv,可以替换为它获得 quoted values / escape 支持。 from pathlib import Path as _P - _dotenv = _P(config_path).parent / ".env" - load_dotenv(_dotenv) + _config_dir = _P(config_path).parent + for _candidate in (".env", ".env.dev", ".env.local"): + load_dotenv(_config_dir / _candidate) server_config = ServerConfig.from_yaml(config_path) app = FastAPI(title="AgentKit Server", version="2.0.0", lifespan=lifespan) diff --git a/src/agentkit/server/auth/jwt_utils.py b/src/agentkit/server/auth/jwt_utils.py index 2c391fa..71e9d68 100644 --- a/src/agentkit/server/auth/jwt_utils.py +++ b/src/agentkit/server/auth/jwt_utils.py @@ -179,6 +179,11 @@ def create_token_pair( "type": "refresh", "iat": int(issued_at.timestamp()), "exp": int(refresh_exp.timestamp()), + # Persist the remember_me flag so /auth/refresh can inherit + # the original TTL (30d vs 7d) without the client re-sending + # it. Without this claim, every refresh would reset to the + # default 7-day TTL, defeating the "记住我 30 天" checkbox. + "rmb": remember_me, } if effective_session_id: access_payload["sid"] = effective_session_id diff --git a/src/agentkit/server/client.py b/src/agentkit/server/client.py index 8c813a6..fbeeb92 100644 --- a/src/agentkit/server/client.py +++ b/src/agentkit/server/client.py @@ -8,7 +8,7 @@ import httpx class AgentKitClient: """Python SDK for AgentKit Server""" - def __init__(self, base_url: str = "http://localhost:8000"): + def __init__(self, base_url: str = "http://localhost:18001"): self._base_url = base_url.rstrip("/") self._client = httpx.AsyncClient(base_url=self._base_url) diff --git a/src/agentkit/server/config.py b/src/agentkit/server/config.py index da2ee38..55d4865 100644 --- a/src/agentkit/server/config.py +++ b/src/agentkit/server/config.py @@ -91,7 +91,7 @@ class ServerConfig: def __init__( self, host: str = "0.0.0.0", - port: int = 8001, + port: int = 18001, workers: int = 1, api_key: str | None = None, rate_limit: int = 60, @@ -264,7 +264,7 @@ class ServerConfig: return cls( host=server.get("host", "0.0.0.0"), - port=server.get("port", 8001), + port=server.get("port", 18001), workers=server.get("workers", 1), api_key=server.get("api_key"), rate_limit=server.get("rate_limit", 60), diff --git a/src/agentkit/server/frontend/components.d.ts b/src/agentkit/server/frontend/components.d.ts index 3b4be9c..2daf35a 100644 --- a/src/agentkit/server/frontend/components.d.ts +++ b/src/agentkit/server/frontend/components.d.ts @@ -131,6 +131,7 @@ declare module 'vue' { OptimizationPanel: typeof import('./src/components/evolution/OptimizationPanel.vue')['default'] ParallelNode: typeof import('./src/components/workflow/ParallelNode.vue')['default'] PathOptimizerPanel: typeof import('./src/components/evolution/PathOptimizerPanel.vue')['default'] + PhaseIndicator: typeof import('./src/components/chat/PhaseIndicator.vue')['default'] PitfallPanel: typeof import('./src/components/evolution/PitfallPanel.vue')['default'] PitfallRoutePanel: typeof import('./src/components/evolution/PitfallRoutePanel.vue')['default'] PlanVisualization: typeof import('./src/components/chat/PlanVisualization.vue')['default'] @@ -160,6 +161,7 @@ declare module 'vue' { SourceConfig: typeof import('./src/components/kb/SourceConfig.vue')['default'] SplashScreen: typeof import('./src/components/layout/SplashScreen.vue')['default'] SplitPane: typeof import('./src/components/layout/SplitPane.vue')['default'] + StickyModeHeader: typeof import('./src/components/chat/StickyModeHeader.vue')['default'] SyncSettings: typeof import('./src/components/calendar/SyncSettings.vue')['default'] SystemMonitorPanel: typeof import('./src/components/layout/SystemMonitorPanel.vue')['default'] SystemTab: typeof import('./src/components/layout/tabs/SystemTab.vue')['default'] diff --git a/src/agentkit/server/frontend/e2e/calendar-data-consistency.spec.ts b/src/agentkit/server/frontend/e2e/calendar-data-consistency.spec.ts index e07d729..76ff574 100644 --- a/src/agentkit/server/frontend/e2e/calendar-data-consistency.spec.ts +++ b/src/agentkit/server/frontend/e2e/calendar-data-consistency.spec.ts @@ -28,7 +28,7 @@ import { TEST_USER, clearAuth } from './helpers' // ── API helpers ──────────────────────────────────────────────────────── -const API_BASE = 'http://127.0.0.1:8000/api/v1' +const API_BASE = `http://127.0.0.1:${process.env.BACKEND_PORT ?? '18001'}/api/v1` const CALENDAR_BASE = `${API_BASE}/calendar` const AUTH_BASE = `${API_BASE}/auth` diff --git a/src/agentkit/server/frontend/e2e/conversation-management.spec.ts b/src/agentkit/server/frontend/e2e/conversation-management.spec.ts index 4324e72..e56813d 100644 --- a/src/agentkit/server/frontend/e2e/conversation-management.spec.ts +++ b/src/agentkit/server/frontend/e2e/conversation-management.spec.ts @@ -24,7 +24,7 @@ import { TEST_USER, clearAuth, reloadAndWaitAuth } from './helpers' // ── API helpers ──────────────────────────────────────────────────────── -const PORTAL_BASE = 'http://127.0.0.1:8000/api/v1/portal' +const PORTAL_BASE = `http://127.0.0.1:${process.env.BACKEND_PORT ?? '18001'}/api/v1/portal` /** Cached access token for API calls (login once per worker). */ let _cachedToken: string | null = null diff --git a/src/agentkit/server/frontend/e2e/global-setup.ts b/src/agentkit/server/frontend/e2e/global-setup.ts index cff8c6b..1aaf16b 100644 --- a/src/agentkit/server/frontend/e2e/global-setup.ts +++ b/src/agentkit/server/frontend/e2e/global-setup.ts @@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) -const BACKEND_HEALTH_URL = 'http://127.0.0.1:8000/api/v1/health' +const BACKEND_HEALTH_URL = `http://127.0.0.1:${process.env.BACKEND_PORT ?? '18001'}/api/v1/health` const SETUP_SCRIPT = resolve(__dirname, 'setup-test-user.py') /** Poll a URL until it returns 200 or the timeout expires. */ diff --git a/src/agentkit/server/frontend/e2e/helpers.ts b/src/agentkit/server/frontend/e2e/helpers.ts index 1ee676e..f7e5cc1 100644 --- a/src/agentkit/server/frontend/e2e/helpers.ts +++ b/src/agentkit/server/frontend/e2e/helpers.ts @@ -14,14 +14,22 @@ import type { Page, expect as ExpectType } from '@playwright/test' // --------------------------------------------------------------------------- /** - * Backend API base — absolute URL so fetch() works in both Node.js (Playwright - * test context) and browser context. The Vite dev-server proxy is not available - * in Node.js, so we target the backend directly. + * Backend port — read from BACKEND_PORT env var (set by the dev / E2E + * environment) so E2E tests follow the same port allocation as the Vite + * proxy and the running backend server. Default 18001 matches + * agentkit.yaml / .env.dev. The Vite dev-server proxy is not available in + * Node.js, so we target the backend directly. */ -export const API_BASE = 'http://127.0.0.1:8000/api/v1' +const BACKEND_PORT = process.env.BACKEND_PORT ?? '18001' + +/** + * Backend API base — absolute URL so fetch() works in both Node.js (Playwright + * test context) and browser context. + */ +export const API_BASE = `http://127.0.0.1:${BACKEND_PORT}/api/v1` /** Backend health endpoint (absolute URL for direct fetch). */ -export const BACKEND_HEALTH_URL = 'http://127.0.0.1:8000/api/v1/health' +export const BACKEND_HEALTH_URL = `http://127.0.0.1:${BACKEND_PORT}/api/v1/health` /** Test admin credentials — must match setup-test-user.py defaults. */ export const TEST_USER = { diff --git a/src/agentkit/server/frontend/playwright.config.ts b/src/agentkit/server/frontend/playwright.config.ts index 0f1c4e4..baf0f40 100644 --- a/src/agentkit/server/frontend/playwright.config.ts +++ b/src/agentkit/server/frontend/playwright.config.ts @@ -5,9 +5,9 @@ import { defineConfig, devices } from '@playwright/test' * * Architecture: * - Backend (uvicorn direct, avoids agentkit serve interactive prompts) runs on - * port 8000 to match the Vite dev-server proxy target in vite.config.ts. - * - Frontend (Vite dev server) runs on port 5173 (strictPort in vite.config.ts). - * - Tests target the frontend at http://localhost:5173; API/WS calls are + * port 18001 to match the Vite dev-server proxy target in vite.config.ts. + * - Frontend (Vite dev server) runs on port 15173 (strictPort in vite.config.ts). + * - Tests target the frontend at http://localhost:15173; API/WS calls are * transparently proxied to the backend. * * The `globalSetup` script creates a test admin user in the auth DB before @@ -30,7 +30,7 @@ export default defineConfig({ globalSetup: './e2e/global-setup.ts', use: { - baseURL: 'http://localhost:5173', + baseURL: 'http://localhost:15173', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', @@ -70,8 +70,8 @@ export default defineConfig({ 'AGENTKIT_GUI_MODE=1 NO_PROXY=127.0.0.1,localhost no_proxy=127.0.0.1,localhost ' + '.venv/bin/python -c "from agentkit.server.app import create_app; import uvicorn; ' + 'app = create_app(rate_limit=10000); ' + - 'uvicorn.run(app, host=\'127.0.0.1\', port=8000)"', - url: 'http://127.0.0.1:8000/api/v1/health', + 'uvicorn.run(app, host=\'127.0.0.1\', port=18001)"', + url: 'http://127.0.0.1:18001/api/v1/health', cwd: PROJECT_ROOT, reuseExistingServer: !process.env.CI, timeout: 120_000, @@ -80,7 +80,7 @@ export default defineConfig({ }, { command: 'npm run dev', - url: 'http://localhost:5173', + url: 'http://localhost:15173', cwd: '.', reuseExistingServer: !process.env.CI, timeout: 60_000, diff --git a/src/agentkit/server/frontend/src-tauri/src/lib.rs b/src/agentkit/server/frontend/src-tauri/src/lib.rs index 12f5368..2d758cb 100644 --- a/src/agentkit/server/frontend/src-tauri/src/lib.rs +++ b/src/agentkit/server/frontend/src-tauri/src/lib.rs @@ -22,20 +22,24 @@ mod auth; /// - clear_refresh_token() -> Result<(), String> /// Remote server connection info. Defaults match the local dev server -/// (``agentkit serve --port 8000``). Override at compile time via +/// (``agentkit serve --port 18001``). Override at compile time via /// ``AGENTKIT_REMOTE_HOST`` / ``AGENTKIT_REMOTE_PORT``. +/// +/// Note: 18001 is an intentionally uncommon port to avoid clashes with +/// common dev services (Docker's geo_backend:8000, pms-*:3306/6379/9200, +/// Vite default 5173, etc.). See vite.config.ts for the matching dev port. const REMOTE_HOST: &str = match option_env!("AGENTKIT_REMOTE_HOST") { Some(h) => h, None => "127.0.0.1", }; const REMOTE_PORT_STR: &str = match option_env!("AGENTKIT_REMOTE_PORT") { Some(p) => p, - None => "8000", + None => "18001", }; /// Parse the port at first use (avoids non-const fn call in constant). fn remote_port() -> u16 { - REMOTE_PORT_STR.parse().unwrap_or(8000) + REMOTE_PORT_STR.parse().unwrap_or(18001) } /// Global state: tracks whether the "backend" is considered started. diff --git a/src/agentkit/server/frontend/src-tauri/tauri.conf.json b/src/agentkit/server/frontend/src-tauri/tauri.conf.json index 0f92693..ffcd9cd 100644 --- a/src/agentkit/server/frontend/src-tauri/tauri.conf.json +++ b/src/agentkit/server/frontend/src-tauri/tauri.conf.json @@ -5,7 +5,7 @@ "identifier": "com.fischer.agentkit", "build": { "frontendDist": "../static", - "devUrl": "http://localhost:5173", + "devUrl": "http://localhost:15173", "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build:frontend" }, diff --git a/src/agentkit/server/frontend/src/App.vue b/src/agentkit/server/frontend/src/App.vue index 5c946aa..6f52dae 100644 --- a/src/agentkit/server/frontend/src/App.vue +++ b/src/agentkit/server/frontend/src/App.vue @@ -143,7 +143,7 @@ body { } .ant-card-hoverable:hover { - border-color: var(--color-primary-light, #eef2ff) !important; + border-color: var(--color-primary-light, #f3f4f6) !important; box-shadow: var(--shadow-md, 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04)) !important; } diff --git a/src/agentkit/server/frontend/src/api/types.ts b/src/agentkit/server/frontend/src/api/types.ts index 6fbc649..e8e4401 100644 --- a/src/agentkit/server/frontend/src/api/types.ts +++ b/src/agentkit/server/frontend/src/api/types.ts @@ -1,477 +1,583 @@ -import type { ICalendarEvent, IInvitation } from './calendar' +import type { ICalendarEvent, IInvitation } from "./calendar"; /** Chat request payload */ export interface IChatRequest { - message: string - conversation_id?: string - sources?: string[] - skill_name?: string + message: string; + conversation_id?: string; + sources?: string[]; + skill_name?: string; } /** Chat response from API */ export interface IChatResponse { - conversation_id: string - message: string - matched_skill?: string - routing_method?: string - confidence?: number - task_id?: string - status: 'completed' | 'pending' + conversation_id: string; + message: string; + matched_skill?: string; + routing_method?: string; + confidence?: number; + task_id?: string; + status: "completed" | "pending"; } /** Tool call data within a message */ export interface IToolCallData { - id: string - name: string - status: 'pending' | 'running' | 'completed' | 'error' - params?: string - result?: string - error?: string - duration?: number - children?: IToolCallData[] + id: string; + name: string; + status: "pending" | "running" | "completed" | "error"; + params?: string; + result?: string; + error?: string; + duration?: number; + children?: IToolCallData[]; } /** Single chat message */ export interface IChatMessage { - id: string - role: 'user' | 'assistant' - content: string - timestamp: string - matched_skill?: string - routing_method?: string - confidence?: number - task_id?: string - status?: 'completed' | 'pending' | 'error' | 'streaming' - tool_calls?: IToolCallData[] - thinking?: string - expert_id?: string - expert_name?: string - expert_color?: string - expert_avatar?: string + id: string; + role: "user" | "assistant"; + content: string; + timestamp: string; + matched_skill?: string; + routing_method?: string; + confidence?: number; + task_id?: string; + status?: "completed" | "pending" | "error" | "streaming"; + tool_calls?: IToolCallData[]; + thinking?: string; + expert_id?: string; + expert_name?: string; + expert_color?: string; + expert_avatar?: string; message_type?: - | 'chat' - | 'handoff' - | 'assist_request' - | 'plan_update' - | 'milestone' - | 'board_started' - | 'board_speech' - | 'board_summary' - | 'board_conclusion' - | 'debate_started' - | 'debate_argument' - | 'debate_summary' - | 'debate_resolved' - | 'collaboration_graph' - | 'review_result' - | 'risk_flagged' - | 'error' - board_round?: number - board_role?: 'moderator' | 'expert' | 'user' | 'summary' - plan_phases?: ITeamPlanPhase[] - error_detail?: string - board_started?: IBoardStartedData - board_conclusion?: IBoardConcludedData - debate_topic?: string - debate_round?: number - debate_decision?: string - debate_rationale?: string - debate_participants?: string[] - debate_opening?: string - debate_moderator?: string + | "chat" + | "handoff" + | "assist_request" + | "plan_update" + | "milestone" + | "board_started" + | "board_speech" + | "board_summary" + | "board_conclusion" + | "debate_started" + | "debate_argument" + | "debate_summary" + | "debate_resolved" + | "collaboration_graph" + | "review_result" + | "risk_flagged" + | "error"; + board_round?: number; + board_role?: "moderator" | "expert" | "user" | "summary"; + plan_phases?: ITeamPlanPhase[]; + error_detail?: string; + board_started?: IBoardStartedData; + board_conclusion?: IBoardConcludedData; + debate_topic?: string; + debate_round?: number; + debate_decision?: string; + debate_rationale?: string; + debate_participants?: string[]; + debate_opening?: string; + debate_moderator?: string; /** U5: PM collaboration — aggregated graph data for CollaborationGraphCard */ - collaboration_graph?: ICollaborationGraphData + collaboration_graph?: ICollaborationGraphData; /** U5: PM collaboration — review result for ReviewResultCard */ - review_result?: IReviewResult + review_result?: IReviewResult; /** U5: PM collaboration — risk flag for RiskFlagCard */ - risk_flag?: IRiskFlag + risk_flag?: IRiskFlag; /** U4: synthesis identifier for streaming milestone dedup (team_synthesis_chunk/team_synthesis) */ - synthesis_id?: string + synthesis_id?: string; } /** Conversation with messages */ export interface IConversation { - id: string - title: string - messages: IChatMessage[] - created_at: string - updated_at: string + id: string; + title: string; + messages: IChatMessage[]; + created_at: string; + updated_at: string; /** 仅本地存在、尚未同步到服务端的临时会话 */ - is_local?: boolean + is_local?: boolean; + /** True if this conversation ever ran a @board meeting (board_started persisted). + * Backed by the server-side `is_board` field in + * /api/v1/portal/conversations so the sidebar can show a "私董会" badge + * without loading every conversation's full history. */ + is_board?: boolean; } /** Capability info */ export interface ICapabilityInfo { - name: string - display_name: string - description: string - icon: string - enabled: boolean - skill_count: number + name: string; + display_name: string; + description: string; + icon: string; + enabled: boolean; + skill_count: number; } /** Capabilities response */ export interface ICapabilitiesResponse { - capabilities: ICapabilityInfo[] + capabilities: ICapabilityInfo[]; } /** WebSocket client message types */ -export type WsClientMessage = { - type: 'chat' - message: string - sources?: string[] - conversation_id?: string - model?: string -} | { - type: 'resume' - task_id: string - conversation_id?: string -} | { - type: 'cancel' - task_id?: string -} | { - type: 'ping' -} +export type WsClientMessage = + | { + type: "chat"; + message: string; + sources?: string[]; + conversation_id?: string; + model?: string; + } + | { + type: "resume"; + task_id: string; + conversation_id?: string; + } + | { + type: "cancel"; + task_id?: string; + } + | { + type: "ping"; + }; /** WebSocket server message types — matches backend portal.py protocol */ export type WsServerMessage = - | { type: 'connected'; conversation_id: string } - | { type: 'routing'; skill: string; confidence: number; method: string } - | { type: 'step'; data: { event_type: string; step: number; data: Record; timestamp: string } } - | { type: 'result'; data: { message?: string; content?: string; status?: string } } - | { type: 'error'; data: { message: string; code?: string } } - | { type: 'pong' } - | { type: 'team_formed'; data: IExpertTeamState } - | { type: 'expert_step'; data: { expert_id: string; expert_name: string; expert_color: string; content: string; step: string } } - | { type: 'expert_result_chunk'; data: { expert_id: string; content: string } } - | { type: 'expert_result_chunk_reset'; data: { expert_id: string; phase_id?: string } } - | { type: 'expert_result'; data: { expert_id: string; expert_name: string; expert_color: string; content: string; status?: 'completed' | 'error'; phase_id?: string; rework_attempt?: number; error?: string } } - | { type: 'plan_update'; data: { plan_phases: ITeamPlanPhase[] } } - | { type: 'phase_started'; data: { phase_id: string; phase_name: string; assigned_expert: string; depends_on: string[] } } - | { type: 'phase_completed'; data: { phase_id: string; phase_name: string; result_summary: string } } - | { type: 'phase_failed'; data: { phase_id: string; phase_name: string; error: string } } + | { type: "connected"; conversation_id: string } + | { type: "routing"; skill: string; confidence: number; method: string } + | { + type: "step"; + data: { + event_type: string; + step: number; + data: Record; + timestamp: string; + }; + } + | { + type: "result"; + data: { message?: string; content?: string; status?: string }; + } + | { type: "error"; data: { message: string; code?: string } } + | { type: "pong" } + | { type: "team_formed"; data: IExpertTeamState } + | { + type: "expert_step"; + data: { + expert_id: string; + expert_name: string; + expert_color: string; + content: string; + step: string; + }; + } + | { + type: "expert_result_chunk"; + data: { expert_id: string; content: string }; + } + | { + type: "expert_result_chunk_reset"; + data: { expert_id: string; phase_id?: string }; + } + | { + type: "expert_result"; + data: { + expert_id: string; + expert_name: string; + expert_color: string; + content: string; + status?: "completed" | "error"; + phase_id?: string; + rework_attempt?: number; + error?: string; + }; + } + | { type: "plan_update"; data: { plan_phases: ITeamPlanPhase[] } } + | { + type: "phase_started"; + data: { + phase_id: string; + phase_name: string; + assigned_expert: string; + depends_on: string[]; + }; + } + | { + type: "phase_completed"; + data: { phase_id: string; phase_name: string; result_summary: string }; + } + | { + type: "phase_failed"; + data: { phase_id: string; phase_name: string; error: string }; + } // PLAN_EXEC (U4) — phase lifecycle events emitted by ReActEngine. - | { type: 'phase_changed'; data: { phase: string; previous: string } } - | { type: 'phase_violation'; data: { current_phase: string; tool: string; message: string; violation_kind: string; command_preview?: string } } - | { type: 'team_synthesis_chunk'; data: { chunk: string; synthesis_id?: string } } - | { type: 'team_synthesis'; data: { content: string; phases_completed?: number; phases_total?: number; synthesis_id?: string; status?: 'completed' | 'error' | 'cancelled'; error?: string } } - | { type: 'team_dissolved'; data: { team_id: string } } + | { type: "phase_changed"; data: { phase: string; previous: string } } + | { + type: "phase_violation"; + data: { + current_phase: string; + tool: string; + message: string; + violation_kind: string; + command_preview?: string; + }; + } + | { + type: "team_synthesis_chunk"; + data: { chunk: string; synthesis_id?: string }; + } + | { + type: "team_synthesis"; + data: { + content: string; + phases_completed?: number; + phases_total?: number; + synthesis_id?: string; + status?: "completed" | "error" | "cancelled"; + error?: string; + }; + } + | { type: "team_dissolved"; data: { team_id: string } } // Board Meeting 模式事件 - | { type: 'board_started'; data: IBoardStartedData } - | { type: 'expert_speech'; data: IExpertSpeechData } - | { type: 'round_summary'; data: IRoundSummaryData } - | { type: 'user_intervention'; data: IUserInterventionData } - | { type: 'board_concluded'; data: IBoardConcludedData } + | { type: "board_started"; data: IBoardStartedData } + | { type: "expert_speech"; data: IExpertSpeechData } + | { type: "expert_speech_chunk"; data: IExpertSpeechData } + | { type: "round_summary"; data: IRoundSummaryData } + | { type: "user_intervention"; data: IUserInterventionData } + | { type: "board_concluded"; data: IBoardConcludedData } // Debate (U5) 事件 - | { type: 'debate_started'; data: IDebateStartedData } - | { type: 'expert_argument'; data: IDebateArgumentData } - | { type: 'debate_round_summary'; data: IDebateRoundSummaryData } - | { type: 'debate_resolved'; data: IDebateResolvedData } - | { type: 'team_intervention_ack'; data: { content: string } } + | { type: "debate_started"; data: IDebateStartedData } + | { type: "expert_argument"; data: IDebateArgumentData } + | { type: "debate_round_summary"; data: IDebateRoundSummaryData } + | { type: "debate_resolved"; data: IDebateResolvedData } + | { type: "team_intervention_ack"; data: { content: string } } // PM Collaboration (U5) 事件 - | { type: 'collaboration_contract_defined'; data: ICollaborationContractDefinedData } - | { type: 'collaboration_notice'; data: ICollaborationNotice } - | { type: 'review_result'; data: IReviewResult } - | { type: 'risk_flagged'; data: IRiskFlag } + | { + type: "collaboration_contract_defined"; + data: ICollaborationContractDefinedData; + } + | { type: "collaboration_notice"; data: ICollaborationNotice } + | { type: "review_result"; data: IReviewResult } + | { type: "risk_flagged"; data: IRiskFlag } // Calendar 事件 (KTD-10 — piggyback on chat WS) - | { type: 'calendar_event_created'; data: ICalendarEventCreatedData } - | { type: 'calendar_reminder'; data: ICalendarReminderData } - | { type: 'calendar_invitation'; data: ICalendarInvitationData } - | { type: 'calendar_sync_conflict'; data: ICalendarSyncConflictData } + | { type: "calendar_event_created"; data: ICalendarEventCreatedData } + | { type: "calendar_reminder"; data: ICalendarReminderData } + | { type: "calendar_invitation"; data: ICalendarInvitationData } + | { type: "calendar_sync_conflict"; data: ICalendarSyncConflictData }; /** Expert info within a team */ export interface IExpertInfo { - id: string - name: string - persona: string - avatar: string - color: string - is_lead: boolean - bound_skills: string[] - status: 'active' | 'inactive' + id: string; + name: string; + persona: string; + avatar: string; + color: string; + is_lead: boolean; + bound_skills: string[]; + status: "active" | "inactive"; } /** A phase within a team plan */ export interface ITeamPlanPhase { - id: string - name: string - assigned_expert: string - task_description?: string - depends_on: string[] - status: 'pending' | 'in_progress' | 'completed' | 'failed' - result?: string - parallel_type?: 'serial' | 'subtask_parallel' | 'competitive_parallel' - milestone?: string + id: string; + name: string; + assigned_expert: string; + task_description?: string; + depends_on: string[]; + status: "pending" | "in_progress" | "completed" | "failed"; + result?: string; + parallel_type?: "serial" | "subtask_parallel" | "competitive_parallel"; + milestone?: string; /** U5: PM collaboration — contracts defined by Lead for this phase */ - collaboration_contracts?: ICollaborationContract[] + collaboration_contracts?: ICollaborationContract[]; /** U5: PM collaboration — rework count after Lead review failures */ - rework_count?: number + rework_count?: number; /** U5: PM collaboration — Lead review feedback (modification requirements) */ - review_feedback?: string | null + review_feedback?: string | null; } /** Expert team state */ export interface IExpertTeamState { - team_id: string - status: 'forming' | 'planning' | 'executing' | 'synthesizing' | 'completed' | 'dissolved' - experts: IExpertInfo[] - plan_phases: ITeamPlanPhase[] - lead_expert: string + team_id: string; + status: + | "forming" + | "planning" + | "executing" + | "synthesizing" + | "completed" + | "dissolved"; + experts: IExpertInfo[]; + plan_phases: ITeamPlanPhase[]; + lead_expert: string; /** U2: 团队级任务目标摘要(可选,后端 team_formed 事件未发送时回退到首阶段描述) */ - task_description?: string + task_description?: string; } // ── Board Meeting 模式类型 ──────────────────────────────────────────── /** Board meeting expert info (lighter than IExpertInfo) */ export interface IBoardExpert { - name: string - avatar: string - color: string - is_moderator: boolean - persona: string + name: string; + avatar: string; + color: string; + is_moderator: boolean; + persona: string; } /** board_started event payload */ export interface IBoardStartedData { - team_id: string - topic: string - experts: IBoardExpert[] - max_rounds: number + team_id: string; + topic: string; + experts: IBoardExpert[]; + max_rounds: number; } /** expert_speech event payload */ export interface IExpertSpeechData { - expert_name: string - expert_avatar: string - expert_color: string - content: string - round: number - role: 'moderator' | 'expert' + expert_name: string; + expert_avatar: string; + expert_color: string; + content: string; + round: number; + role: "moderator" | "expert"; } /** round_summary event payload */ export interface IRoundSummaryData { - moderator_name: string - content: string - round: number - continue: boolean + moderator_name: string; + content: string; + round: number; + continue: boolean; } /** user_intervention event payload */ export interface IUserInterventionData { - content: string - round: number + content: string; + round: number; } /** board_concluded event payload */ export interface IBoardConcludedData { - summary: string - decision_advice: string - total_rounds: number - consensus_points: string[] - dissent_points: string[] - error?: string + summary: string; + decision_advice: string; + total_rounds: number; + consensus_points: string[]; + dissent_points: string[]; + error?: string; } // ── Debate (U5) 模式类型 ────────────────────────────────────────────── /** debate_started event payload */ export interface IDebateStartedData { - phase_id: string - phase_name: string - topic: string - participants: string[] - max_rounds: number - opening: string + phase_id: string; + phase_name: string; + topic: string; + participants: string[]; + max_rounds: number; + opening: string; } /** expert_argument event payload */ export interface IDebateArgumentData { - phase_id: string - expert_id: string - expert_name: string - expert_color: string - content: string - round: number - topic: string + phase_id: string; + expert_id: string; + expert_name: string; + expert_color: string; + content: string; + round: number; + topic: string; } /** debate_round_summary event payload */ export interface IDebateRoundSummaryData { - phase_id: string - moderator_name: string - content: string - round: number - continue: boolean + phase_id: string; + moderator_name: string; + content: string; + round: number; + continue: boolean; } /** debate_resolved event payload */ export interface IDebateResolvedData { - phase_id: string - phase_name: string - decision: 'adopt' | 'compromise' | 'shelve' | 'inconclusive' - conclusion: string - rationale: string + phase_id: string; + phase_name: string; + decision: "adopt" | "compromise" | "shelve" | "inconclusive"; + conclusion: string; + rationale: string; } // ── PM Collaboration (U5) 模式类型 ────────────────────────────────── /** 协作契约 — 匹配后端 CollaborationContract.to_dict() */ export interface ICollaborationContract { - from_expert: string - to_expert: string - content_description: string - status: 'pending' | 'delivered' | 'received' + from_expert: string; + to_expert: string; + content_description: string; + status: "pending" | "delivered" | "received"; } /** collaboration_contract_defined event payload * (后端当前通过 plan_update 的 plan_phases[].collaboration_contracts 携带, * 此类型用于可能的独立事件和类型完整性) */ export interface ICollaborationContractDefinedData { - phase_id: string - phase_name: string - contracts: ICollaborationContract[] + phase_id: string; + phase_name: string; + contracts: ICollaborationContract[]; } /** collaboration_notice event payload — 专家完成后按契约通知相关专家 */ export interface ICollaborationNotice { - from_expert: string - to_expert: string - content_description: string - phase_id: string - phase_name: string - output_key: string - expert_color: string + from_expert: string; + to_expert: string; + content_description: string; + phase_id: string; + phase_name: string; + output_key: string; + expert_color: string; } /** review_result event payload — Lead 验收阶段输出 */ export interface IReviewResult { - phase_id: string - phase_name: string - passed: boolean - feedback: string - expert: string - rework_count?: number - final_status?: 'rework' | 'failed' + phase_id: string; + phase_name: string; + passed: boolean; + feedback: string; + expert: string; + rework_count?: number; + final_status?: "rework" | "failed"; } /** risk_flagged event payload — 专家风险标记 */ export interface IRiskFlag { - expert: string - expert_name: string - risk_description: string - phase_id: string - phase_name: string + expert: string; + expert_name: string; + risk_description: string; + phase_id: string; + phase_name: string; } /** 协作关系图聚合数据 — 存储在 collaboration_graph 消息中,随事件实时更新 */ export interface ICollaborationGraphData { - contracts: Array - notices: ICollaborationNotice[] - reviews: IReviewResult[] - risks: IRiskFlag[] + contracts: Array< + ICollaborationContract & { phase_id: string; phase_name: string } + >; + notices: ICollaborationNotice[]; + reviews: IReviewResult[]; + risks: IRiskFlag[]; } /** Board meeting status (matches backend BoardStatus enum) */ -export type BoardStatus = 'forming' | 'discussing' | 'concluding' | 'completed' | 'dissolved' +export type BoardStatus = + | "forming" + | "discussing" + | "concluding" + | "completed" + | "dissolved"; /** Board message entry for group chat display */ export interface IBoardMessage { - id: string - expert_name: string - expert_avatar: string - expert_color: string - content: string - round: number - role: 'moderator' | 'expert' | 'user' | 'summary' - timestamp: number + id: string; + expert_name: string; + expert_avatar: string; + expert_color: string; + content: string; + round: number; + role: "moderator" | "expert" | "user" | "summary"; + timestamp: number; } // ── Calendar WS 事件 payload 类型 ─────────────────────────────────── /** calendar_event_created payload */ export interface ICalendarEventCreatedData { - event: ICalendarEvent + event: ICalendarEvent; } /** calendar_reminder payload */ export interface ICalendarReminderData { - event_id: string - title: string - start_time: string - offset_minutes: number - channels: string[] + event_id: string; + title: string; + start_time: string; + offset_minutes: number; + channels: string[]; } /** calendar_invitation payload (G6) */ export interface ICalendarInvitationData { - invitation: IInvitation - event_title: string - inviter_name: string + invitation: IInvitation; + event_title: string; + inviter_name: string; } /** calendar_sync_conflict payload (G4) */ export interface ICalendarSyncConflictData { - event_id: string - event_title: string - provider: string - local_modified: string - remote_modified: string - resolution: string + event_id: string; + event_title: string; + provider: string; + local_modified: string; + remote_modified: string; + resolution: string; } /** Expert template (matches backend GET /api/v1/experts response item) */ export interface IExpertTemplate { - name: string - description: string - is_builtin: boolean - avatar: string - color: string - persona: string - thinking_style: string - speaking_style: string - decision_framework: string + name: string; + description: string; + is_builtin: boolean; + avatar: string; + color: string; + persona: string; + thinking_style: string; + speaking_style: string; + decision_framework: string; } /** Experts list response (matches backend GET /api/v1/experts) */ export interface IExpertsResponse { - experts: IExpertTemplate[] - total: number + experts: IExpertTemplate[]; + total: number; } /** File upload response (matches backend POST /api/v1/chat/upload) */ export interface IUploadResponse { - filename: string - stored_name: string - content_type: string - size: number - download_url: string + filename: string; + stored_name: string; + content_type: string; + size: number; + download_url: string; } /** API error */ export interface IApiError { - status: number - message: string - detail?: string + status: number; + message: string; + detail?: string; } /** Task status (matches backend TaskStatus enum) */ -export type TaskStatus = 'pending' | 'running' | 'completed' | 'partially_completed' | 'failed' | 'cancelled' +export type TaskStatus = + | "pending" + | "running" + | "completed" + | "partially_completed" + | "failed" + | "cancelled"; /** Task record (matches backend TaskRecord.to_dict()) */ export interface ITaskRecord { - task_id: string - agent_name: string - skill_name: string | null - input_data: Record - status: TaskStatus - output_data: Record | null - error_message: string | null - created_at: string - started_at: string | null - completed_at: string | null - progress: number - progress_message: string - metadata: Record + task_id: string; + agent_name: string; + skill_name: string | null; + input_data: Record; + status: TaskStatus; + output_data: Record | null; + error_message: string | null; + created_at: string; + started_at: string | null; + completed_at: string | null; + progress: number; + progress_message: string; + metadata: Record; } diff --git a/src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue b/src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue index e0cd72b..a5b82cf 100644 --- a/src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue +++ b/src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue @@ -46,7 +46,7 @@ function formatSize(bytes: number): string { display: inline-flex; align-items: center; gap: 4px; - color: var(--color-primary, #1677ff); + color: var(--color-primary, #1a1a1a); text-decoration: none; font-size: 12px; } diff --git a/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue b/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue index fddc186..b8d4e77 100644 --- a/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue +++ b/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue @@ -301,11 +301,11 @@ defineExpose({ } .bitable-grid-scope :deep(.vxe-body--column.is--dirty) { - background: var(--bg-tertiary, #fffbe6); + background: var(--bg-tertiary, #f3f4f6); } .bitable-grid-scope :deep(.vxe-cell--dirty) { - color: var(--color-primary, #1677ff); + color: var(--color-primary, #1a1a1a); } .bitable-grid-scope__add-col { @@ -321,6 +321,6 @@ defineExpose({ } .bitable-grid-scope__add-col:hover { - color: var(--color-primary, #1677ff); + color: var(--color-primary, #1a1a1a); } diff --git a/src/agentkit/server/frontend/src/components/bitable/FieldManagePanel.vue b/src/agentkit/server/frontend/src/components/bitable/FieldManagePanel.vue index 2923fd1..8f15890 100644 --- a/src/agentkit/server/frontend/src/components/bitable/FieldManagePanel.vue +++ b/src/agentkit/server/frontend/src/components/bitable/FieldManagePanel.vue @@ -17,7 +17,7 @@ {{ f.name }}
{{ typeLabel(f.field_type) }} - + {{ f.owner === 'agent' ? 'Agent' : '用户' }}
diff --git a/src/agentkit/server/frontend/src/components/bitable/FileCard.vue b/src/agentkit/server/frontend/src/components/bitable/FileCard.vue index ed09256..88bf9d7 100644 --- a/src/agentkit/server/frontend/src/components/bitable/FileCard.vue +++ b/src/agentkit/server/frontend/src/components/bitable/FileCard.vue @@ -1,7 +1,9 @@ @@ -139,7 +179,10 @@ function statusIcon(status: string): string { width: 22px; height: 22px; border-radius: var(--radius-full); - font-size: 12px; + font-size: 11px; + font-weight: 600; + font-family: var(--font-mono); + color: #fff; background: var(--accent-team-soft); } diff --git a/src/agentkit/server/frontend/src/components/chat/messages/UserBubble.vue b/src/agentkit/server/frontend/src/components/chat/messages/UserBubble.vue index dbafc7a..e2e814b 100644 --- a/src/agentkit/server/frontend/src/components/chat/messages/UserBubble.vue +++ b/src/agentkit/server/frontend/src/components/chat/messages/UserBubble.vue @@ -4,8 +4,8 @@ class="user-bubble" :class="{ 'user-bubble--focusable': msgId }" :tabindex="msgId ? 0 : undefined" - @mouseenter="hovered = true" - @mouseleave="hovered = false" + @mouseenter="onBubbleMouseEnter" + @mouseleave="onBubbleMouseLeave" @focus="focused = true" @blur="focused = false" @pointerdown="onPointerDown" @@ -16,6 +16,28 @@ :filename="fileAttachment.filename" :url="fileAttachment.url" /> +
+
+ {{ commandBubble.icon }} + {{ commandBubble.label }} + · + {{ commandBubble.topic }} + · + + {{ commandBubble.expertCount }} 位专家 + +
+
    +
  • + + {{ expert }} +
  • +
+
{{ content }}