Compare commits
18 Commits
ecb26534fc
...
1d031f4577
| Author | SHA1 | Date |
|---|---|---|
|
|
1d031f4577 | |
|
|
eed425e965 | |
|
|
3d5a6a54ca | |
|
|
92a0be5274 | |
|
|
ebb9ca7140 | |
|
|
d7d9da67f6 | |
|
|
33705637ea | |
|
|
c667faad65 | |
|
|
dc534e993e | |
|
|
fdff86e5b7 | |
|
|
f96bbafa32 | |
|
|
67e036dc23 | |
|
|
27414fc4e1 | |
|
|
326c780ab7 | |
|
|
94094f7563 | |
|
|
80cfd8b899 | |
|
|
830c8abcf1 | |
|
|
b88fa12214 |
|
|
@ -1,5 +1,27 @@
|
|||
# Milestones
|
||||
|
||||
## v1.0 v1.0 (Shipped: 2026-04-17)
|
||||
|
||||
**Phases completed:** 8 phases, 13 plans, 14 tasks
|
||||
|
||||
**Key accomplishments:**
|
||||
|
||||
- 交付了可复现冲突证据链、文件级风险清单与 Titan 重叠决策矩阵,形成“旧视觉+新逻辑”执行输入。
|
||||
- 线程路由从 isnew 参数切换为路由单路径语义,并将 skills bootstrap 合同统一到 content_ids。
|
||||
- 完成 03-UAT 的关键 gap 收敛:lint 阻塞清零,welcome-and-routing 从 4 失败收敛到 0 失败。
|
||||
- 基于 originui 合并基线完成 Phase 3 执行记录,并输出可审计的视觉与回归验证结果。
|
||||
- 完成 Phase 4 首轮执行:iframe 通信与导出链路加入前端容错,目标 lint/E2E 验证通过。
|
||||
- Phase 5 执行完成:目标 E2E 套件达到“0 失败、可解释 skip”,并形成提交卫生分组建议。
|
||||
- 完成引用提交契约与软失败链路,确保 uploads + references 统一进 `additional_kwargs.files`。
|
||||
- 完成输入框 `@` 引用交互闭环:候选展示、过滤、选择、chip 渲染、删除、键盘操作与上限控制。
|
||||
- 补齐 Phase 6 的验证与提交卫生材料,并记录了可复现的 E2E 环境阻塞证据。
|
||||
- 输入框 `@` 引用链路已收口:候选贴边定位、内嵌引用预览与 6 个上限、artifact 引用可转为上下文可消费的 uploads 契约。
|
||||
- Phase 06 最后一个 gap-closure 计划已收口:输入框引用合同重新对齐 requirement=10,DF-INPUT-008/009 都已变成可重复运行的稳定回归。
|
||||
- Phase 06 的执行文档已闭环,提交顺序与验证证据可直接供后续 verify-work 与审阅使用。
|
||||
- Phase 06 已完成 `@` 文件引用能力(artifacts + uploads)及提交契约收敛,并具备可审计验证材料。
|
||||
|
||||
---
|
||||
|
||||
## v1.0 milestone (Shipped: 2026-04-15)
|
||||
|
||||
**Phases completed:** 6 phases, 10 plans, 14 tasks
|
||||
|
|
|
|||
|
|
@ -67,5 +67,17 @@ Plans:
|
|||
- [x] 06-04-ARCHIVED.md — 修订归档:原 gap-closure 计划与锁定决策 D-08(上限 10)冲突,保留追踪但不再执行
|
||||
- [ ] 06-05-PLAN.md — 关闭 verification 缺口:恢复 10 个上限/类型去歧义,并稳定 DF-INPUT-008/009 回归
|
||||
|
||||
### Phase 7: 发送时拼接附件与Skill优先提示词并在消息区过滤
|
||||
|
||||
**Goal:** 发送消息时拼接附件/Skill优先提示词,同时消息区仅展示用户原文。
|
||||
**Requirements**: P7-01, P7-02, P7-03, P7-04
|
||||
**Depends on:** Phase 6
|
||||
**Plans:** 2/2 plans complete
|
||||
|
||||
Plans:
|
||||
- [x] 07-01-PLAN.md — 提交态增强文本组装 + 三入口统一透传 + 显示态/提交态分离回归
|
||||
- [x] 07-02-PLAN.md — gap closure:修复 ContextMenu 自动引用、提示前缀唯一化、Skill 使用 id 拼接
|
||||
|
||||
---
|
||||
*Next command:* `/gsd-verify-work`
|
||||
*Milestone status:* `in_progress`
|
||||
*Next command:* `/gsd-execute-phase 6`
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
status: Executing Phase 06
|
||||
last_updated: "2026-04-16T06:58:00Z"
|
||||
status: v1.0 milestone in progress
|
||||
last_updated: "2026-04-17T06:09:01.300Z"
|
||||
last_activity: 2026-04-17
|
||||
progress:
|
||||
total_phases: 6
|
||||
completed_phases: 6
|
||||
total_plans: 11
|
||||
completed_plans: 13
|
||||
total_phases: 8
|
||||
completed_phases: 7
|
||||
total_plans: 13
|
||||
completed_plans: 16
|
||||
percent: 100
|
||||
---
|
||||
|
||||
|
|
@ -19,13 +20,13 @@ progress:
|
|||
See: .planning/PROJECT.md (updated 2026-04-07)
|
||||
|
||||
**Core value:** Keep the frontend visually familiar while preserving and hardening new-system behavior end to end.
|
||||
**Current focus:** Phase 06 — 06
|
||||
**Current focus:** Milestone v1.0 in progress
|
||||
|
||||
## Workflow State
|
||||
|
||||
- Current workflow: execute-phase completed (phase 06)
|
||||
- Next workflow: verify-work
|
||||
- Next command: /gsd-verify-work
|
||||
- Current workflow: milestone execution (v1.0)
|
||||
- Next workflow: execute-phase
|
||||
- Next command: /gsd-execute-phase 6
|
||||
|
||||
## Artifacts
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ See: .planning/PROJECT.md (updated 2026-04-07)
|
|||
### Roadmap Evolution
|
||||
|
||||
- Phase 6 added: 在输入框输入@时,可引用已生成文件和已上传附件
|
||||
- Phase 7 added: Phase 06 验收后补丁归档(mention/upload语义与附件预览复用)
|
||||
- Phase 7 added: 发送时拼接附件与Skill优先提示词并在消息区过滤
|
||||
|
||||
### Quick Tasks Completed
|
||||
|
||||
|
|
@ -52,5 +53,6 @@ See: .planning/PROJECT.md (updated 2026-04-07)
|
|||
|---|-------------|------|--------|-----------|
|
||||
| 260415-owq | 归档当前git diff为Phase 06验收后补丁:检查改动、更新06-UAT/06-VERIFICATION/06-SUMMARY(必要时)与STATE,再做原子提交 | 2026-04-15 | atomic | [260415-owq-git-diff-phase-06-06-uat-06-verification](./quick/260415-owq-git-diff-phase-06-06-uat-06-verification/) |
|
||||
| 260416-koe | 归档 Phase 06 明确指代(“这张图”)语义修复到 GSD 流程(已验收,通过人工确认,免验证) | 2026-04-16 | pending | [260416-koe-phase-06](./quick/260416-koe-phase-06/) |
|
||||
| 260417-kcb | suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage | 2026-04-17 | pending | [260417-kcb-suggestion-hover-dropdown-child-tooltip-](./quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/) |
|
||||
|
||||
Last activity: 2026-04-16 - Completed quick task 260416-koe: 归档 Phase 06 明确指代(“这张图”)语义修复到 GSD 流程(已验收,通过人工确认,免验证)
|
||||
Last activity: 2026-04-17 - Completed quick task 260417-kcb: suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
---
|
||||
milestone: v1.0
|
||||
audited: 2026-04-17T06:05:06Z
|
||||
status: gaps_found
|
||||
scores:
|
||||
requirements: 6/17
|
||||
phases: 2/7
|
||||
integration: 1/1
|
||||
flows: 0/2
|
||||
gaps:
|
||||
requirements:
|
||||
- id: "MERGE-02"
|
||||
status: "orphaned"
|
||||
phase: "Phase 1"
|
||||
claimed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-PLAN.md"]
|
||||
completed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-SUMMARY.md"]
|
||||
verification_status: "orphaned"
|
||||
evidence: "Listed in SUMMARY frontmatter, but absent from all phase VERIFICATION.md files (only 01 and 06 verification files exist)."
|
||||
- id: "LOGIC-03"
|
||||
status: "orphaned"
|
||||
phase: "Phase 2"
|
||||
claimed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-PLAN.md"]
|
||||
completed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-SUMMARY.md"]
|
||||
verification_status: "orphaned"
|
||||
evidence: "Traceability marks complete, but no phase VERIFICATION coverage; integration audit also flags xclaw_used compatibility gap."
|
||||
- id: "LOGIC-04"
|
||||
status: "orphaned"
|
||||
phase: "Phase 2"
|
||||
claimed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-PLAN.md"]
|
||||
completed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-SUMMARY.md"]
|
||||
verification_status: "orphaned"
|
||||
evidence: "Claimed in SUMMARY, absent from all VERIFICATION.md; integration audit flags legacy content_id adapter risk."
|
||||
- id: "UI-01"
|
||||
status: "orphaned"
|
||||
phase: "Phase 3"
|
||||
claimed_by_plans: [".planning/phases/03-legacy-visual-alignment-pass/03-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Not listed in requirements-completed frontmatter and no phase VERIFICATION.md exists for Phase 3."
|
||||
- id: "UI-02"
|
||||
status: "orphaned"
|
||||
phase: "Phase 3"
|
||||
claimed_by_plans: [".planning/phases/03-legacy-visual-alignment-pass/03-PLAN.md", ".planning/phases/03-legacy-visual-alignment-pass/03-02-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Mentioned as targeted in summaries but not in requirements-completed frontmatter and no VERIFICATION.md exists."
|
||||
- id: "UI-03"
|
||||
status: "orphaned"
|
||||
phase: "Phase 3"
|
||||
claimed_by_plans: [".planning/phases/03-legacy-visual-alignment-pass/03-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "No requirements-completed frontmatter evidence and no phase VERIFICATION.md exists."
|
||||
- id: "LOGIC-01"
|
||||
status: "orphaned"
|
||||
phase: "Phase 4"
|
||||
claimed_by_plans: [".planning/phases/04-iframe-markdown-new-system-stabilization/04-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Only targeted in summary body; no requirements-completed frontmatter and no phase VERIFICATION.md exists."
|
||||
- id: "LOGIC-02"
|
||||
status: "orphaned"
|
||||
phase: "Phase 4"
|
||||
claimed_by_plans: [".planning/phases/04-iframe-markdown-new-system-stabilization/04-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Only targeted in summary body; no requirements-completed frontmatter and no phase VERIFICATION.md exists."
|
||||
- id: "TEST-01"
|
||||
status: "orphaned"
|
||||
phase: "Phase 5"
|
||||
claimed_by_plans: [".planning/phases/05-test-hardening-and-commit-hygiene/05-PLAN.md", ".planning/phases/03-legacy-visual-alignment-pass/03-02-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Targeted in summary text but not requirements-completed frontmatter and no phase VERIFICATION.md exists."
|
||||
- id: "TEST-02"
|
||||
status: "orphaned"
|
||||
phase: "Phase 5"
|
||||
claimed_by_plans: [".planning/phases/05-test-hardening-and-commit-hygiene/05-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "No phase VERIFICATION.md exists for Phase 5; traceability still pending."
|
||||
- id: "TEST-03"
|
||||
status: "orphaned"
|
||||
phase: "Phase 5"
|
||||
claimed_by_plans: [".planning/phases/05-test-hardening-and-commit-hygiene/05-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "No phase VERIFICATION.md exists for Phase 5; integration audit additionally flags missing 07-VERIFICATION as auditability gap."
|
||||
integration:
|
||||
- from: "Phase 2"
|
||||
to: "Phase 2/7 runtime"
|
||||
issue: "LOGIC-03 requires xclaw_used handling, but runtime consumer is not present in code path."
|
||||
- from: "Phase 2"
|
||||
to: "Phase 4/7 runtime"
|
||||
issue: "Legacy content_id adapter evidence is incomplete; content_ids-only flow may not satisfy LOGIC-04 compatibility claim."
|
||||
flows:
|
||||
- name: "Legacy compatibility flow (thread_id/isnew/xclaw_used)"
|
||||
break_at: "xclaw_used ingestion/propagation"
|
||||
evidence: "No code-path consumer found; flagged by integration checker."
|
||||
- name: "Verification evidence flow"
|
||||
break_at: "Phase verification artifact generation"
|
||||
evidence: "Phases 02/03/04/05/07 are missing *-VERIFICATION.md."
|
||||
tech_debt:
|
||||
- phase: "02-thread-and-skills-logic-reconciliation"
|
||||
items:
|
||||
- "E2E was environment-blocked during summary run (ERR_CONNECTION_REFUSED at 127.0.0.1:2026)."
|
||||
- "Summary/code drift noted for referenced files in integration audit."
|
||||
- phase: "03-legacy-visual-alignment-pass"
|
||||
items:
|
||||
- "Execution relied on merged dirty baseline with blockers deferred across phases."
|
||||
- phase: "04-iframe-markdown-new-system-stabilization"
|
||||
items:
|
||||
- "5 E2E skips recorded for fixture/history-dependent paths."
|
||||
- phase: "05-test-hardening-and-commit-hygiene"
|
||||
items:
|
||||
- "10 E2E skips remain, explained but still deferred reliability debt."
|
||||
- phase: "06-"
|
||||
items:
|
||||
- "06-VALIDATION.md status is draft despite nyquist_compliant true."
|
||||
- phase: "07-phase-06-mention-upload"
|
||||
items:
|
||||
- "07-VALIDATION exists without 07-VERIFICATION artifact."
|
||||
nyquist:
|
||||
compliant_phases: ["06", "07"]
|
||||
partial_phases: []
|
||||
missing_phases: ["01", "02", "03", "04", "05"]
|
||||
overall: "partial"
|
||||
---
|
||||
|
||||
# Milestone v1.0 Audit
|
||||
|
||||
## Scope
|
||||
|
||||
- Milestone: `v1.0`
|
||||
- In-scope phase directories:
|
||||
- `.planning/phases/01-conflict-inventory-and-decision-matrix`
|
||||
- `.planning/phases/02-thread-and-skills-logic-reconciliation`
|
||||
- `.planning/phases/03-legacy-visual-alignment-pass`
|
||||
- `.planning/phases/04-iframe-markdown-new-system-stabilization`
|
||||
- `.planning/phases/05-test-hardening-and-commit-hygiene`
|
||||
- `.planning/phases/06-`
|
||||
- `.planning/phases/07-phase-06-mention-upload`
|
||||
|
||||
## Phase Verification Coverage
|
||||
|
||||
| Phase | VERIFICATION.md | Status |
|
||||
|---|---|---|
|
||||
| 01 | present | passed |
|
||||
| 02 | missing | unverified (blocker) |
|
||||
| 03 | missing | unverified (blocker) |
|
||||
| 04 | missing | unverified (blocker) |
|
||||
| 05 | missing | unverified (blocker) |
|
||||
| 06 | present | passed |
|
||||
| 07 | missing | unverified (blocker) |
|
||||
|
||||
## Requirements 3-Source Cross-Reference
|
||||
|
||||
| REQ-ID | Traceability | VERIFICATION Source | SUMMARY `requirements-completed` | Final |
|
||||
|---|---|---|---|---|
|
||||
| MERGE-01 | Complete | passed (01) | listed | satisfied |
|
||||
| MERGE-02 | Complete | missing/orphaned | listed | unsatisfied (orphaned) |
|
||||
| MERGE-03 | Complete | passed (01) | listed | satisfied |
|
||||
| LOGIC-03 | Complete | missing/orphaned | listed | unsatisfied (orphaned) |
|
||||
| LOGIC-04 | Complete | missing/orphaned | listed | unsatisfied (orphaned) |
|
||||
| UI-01 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| UI-02 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| UI-03 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| LOGIC-01 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| LOGIC-02 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| TEST-01 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| TEST-02 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| TEST-03 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| ATREF-01 | Pending | passed (06) | listed | satisfied (checkbox stale) |
|
||||
| ATREF-02 | Pending | passed (06) | listed | satisfied (checkbox stale) |
|
||||
| ATREF-03 | Pending | passed (06) | listed | satisfied (checkbox stale) |
|
||||
| ATREF-04 | Pending | passed (06) | listed | satisfied (checkbox stale) |
|
||||
|
||||
### FAIL Gate
|
||||
|
||||
`gaps_found` is enforced because unsatisfied requirements exist (11), including orphaned requirements assigned in traceability but absent from all phase VERIFICATION files.
|
||||
|
||||
## Integration Checker Results
|
||||
|
||||
### Critical
|
||||
- No critical integration break found across phases 2 to 7.
|
||||
|
||||
### Non-Critical
|
||||
- LOGIC-03 compatibility gap (`xclaw_used` path not evidenced in runtime).
|
||||
- LOGIC-04 compatibility risk (legacy adapter evidence incomplete).
|
||||
- Phase 2 summary/code artifact drift.
|
||||
- Phase 7 has validation but no verification artifact.
|
||||
|
||||
## Broken Flows
|
||||
|
||||
- Legacy compatibility flow (`thread_id/isnew/xclaw_used`) breaks at xclaw_used ingestion/propagation.
|
||||
- Verification evidence flow breaks at missing phase-level VERIFICATION artifacts.
|
||||
|
||||
## Overall Conclusion
|
||||
|
||||
Milestone `v1.0` is **not ready to complete** under current audit gates. Requirements and integration implementation are substantial, but verification artifacts are incomplete for multiple phases, causing orphaned requirements and mandatory `gaps_found` status.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Requirements Archive: v1.0 milestone
|
||||
# Requirements Archive: v1.0 v1.0
|
||||
|
||||
**Archived:** 2026-04-15
|
||||
**Archived:** 2026-04-17
|
||||
**Status:** SHIPPED
|
||||
|
||||
For current requirements, see `.planning/REQUIREMENTS.md`.
|
||||
|
|
|
|||
|
|
@ -67,5 +67,17 @@ Plans:
|
|||
- [x] 06-04-ARCHIVED.md — 修订归档:原 gap-closure 计划与锁定决策 D-08(上限 10)冲突,保留追踪但不再执行
|
||||
- [ ] 06-05-PLAN.md — 关闭 verification 缺口:恢复 10 个上限/类型去歧义,并稳定 DF-INPUT-008/009 回归
|
||||
|
||||
### Phase 7: 发送时拼接附件与Skill优先提示词并在消息区过滤
|
||||
|
||||
**Goal:** 发送消息时拼接附件/Skill优先提示词,同时消息区仅展示用户原文。
|
||||
**Requirements**: P7-01, P7-02, P7-03, P7-04
|
||||
**Depends on:** Phase 6
|
||||
**Plans:** 2/2 plans complete
|
||||
|
||||
Plans:
|
||||
- [x] 07-01-PLAN.md — 提交态增强文本组装 + 三入口统一透传 + 显示态/提交态分离回归
|
||||
- [x] 07-02-PLAN.md — gap closure:修复 ContextMenu 自动引用、提示前缀唯一化、Skill 使用 id 拼接
|
||||
|
||||
---
|
||||
*Next command:* `/gsd-verify-work`
|
||||
*Milestone status:* `in_progress`
|
||||
*Next command:* `/gsd-execute-phase 6`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
---
|
||||
phase: 07-phase-06-mention-upload
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- frontend/src/components/workspace/input-box.tsx
|
||||
- frontend/src/core/threads/hooks.ts
|
||||
- frontend/src/components/ai-elements/prompt-input.tsx
|
||||
- frontend/src/components/workspace/messages/message-list-item.tsx
|
||||
- frontend/src/core/i18n/locales/zh-CN.ts
|
||||
- frontend/src/core/i18n/locales/en-US.ts
|
||||
- frontend/src/core/i18n/locales/types.ts
|
||||
- frontend/src/core/threads/hooks.test.ts
|
||||
- frontend/tests/e2e/input-and-compose.spec.ts
|
||||
autonomous: true
|
||||
requirements:
|
||||
- P7-01
|
||||
- P7-02
|
||||
- P7-03
|
||||
- P7-04
|
||||
must_haves:
|
||||
truths:
|
||||
- "发送到后端的文本会拼接‘优先使用…附件和…Skill’,但消息区仅展示用户原文。"
|
||||
- "拼接规则固定:附件在前、Skill在后;单类单出;大小写不敏感去重。"
|
||||
- "按钮发送、回车发送、建议词自动发送三条入口行为一致。"
|
||||
artifacts:
|
||||
- path: "frontend/src/core/threads/hooks.ts"
|
||||
provides: "提交态增强文本与展示态原文分离"
|
||||
contains: "payload text composition"
|
||||
- path: "frontend/src/components/workspace/input-box.tsx"
|
||||
provides: "references + selectedSkills 元数据传递"
|
||||
contains: "handleSubmit"
|
||||
- path: "frontend/src/components/workspace/messages/message-list-item.tsx"
|
||||
provides: "人类消息渲染仍以原文为准"
|
||||
contains: "contentToDisplay"
|
||||
key_links:
|
||||
- from: "frontend/src/components/workspace/input-box.tsx"
|
||||
to: "frontend/src/core/threads/hooks.ts"
|
||||
via: "PromptInputMessage 扩展字段"
|
||||
pattern: "selectedSkills/references -> payload composition"
|
||||
- from: "frontend/src/core/threads/hooks.ts"
|
||||
to: "frontend/src/components/workspace/messages/message-list-item.tsx"
|
||||
via: "optimistic content + persisted display consistency"
|
||||
pattern: "original text only"
|
||||
---
|
||||
|
||||
<objective>
|
||||
实现 Phase 7 决策:发送时将附件与 Skill 提示文案拼接进提交给后端的提示词,但消息区不展示拼接内容。
|
||||
|
||||
Purpose: 在不破坏既有 `additional_kwargs.files` 语义和输入体验的前提下,增强模型侧提示优先级。
|
||||
Output: 形成稳定的“提交态增强文本/展示态原文”链路,并由单测 + E2E 回归覆盖。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/mt/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/mt/.codex/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/07-phase-06-mention-upload/07-CONTEXT.md
|
||||
@.planning/phases/07-phase-06-mention-upload/07-RESEARCH.md
|
||||
@.planning/phases/07-phase-06-mention-upload/07-VALIDATION.md
|
||||
@frontend/src/components/workspace/input-box.tsx
|
||||
@frontend/src/core/threads/hooks.ts
|
||||
@frontend/src/components/ai-elements/prompt-input.tsx
|
||||
@frontend/src/components/workspace/messages/message-list-item.tsx
|
||||
@frontend/tests/e2e/input-and-compose.spec.ts
|
||||
|
||||
<interfaces>
|
||||
From frontend/src/components/ai-elements/prompt-input.tsx:
|
||||
```typescript
|
||||
export type PromptInputMessage = {
|
||||
text: string;
|
||||
files: FileUIPart[];
|
||||
references?: PromptInputReference[];
|
||||
};
|
||||
```
|
||||
|
||||
From frontend/src/core/threads/hooks.ts:
|
||||
```typescript
|
||||
const sendMessage = async (threadId: string | undefined, message: PromptInputMessage) => {
|
||||
const text = message.text.trim();
|
||||
// optimistic human message + submit payload
|
||||
};
|
||||
```
|
||||
|
||||
From frontend/src/components/workspace/input-box.tsx:
|
||||
```typescript
|
||||
onSubmit?.({ ...message, references });
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: 设计并接入“提交态增强文本”组装器</name>
|
||||
<files>frontend/src/core/threads/hooks.ts, frontend/src/components/ai-elements/prompt-input.tsx</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-phase-06-mention-upload/07-CONTEXT.md
|
||||
- frontend/src/core/threads/hooks.ts
|
||||
- frontend/src/components/ai-elements/prompt-input.tsx
|
||||
- frontend/src/core/threads/submit-files.ts
|
||||
</read_first>
|
||||
<action>
|
||||
扩展 `PromptInputMessage` 以承载发送时需要的 Skill 名列表(例如 `selectedSkills?: Array<{ title: string }>`),并在 `hooks.ts` 中新增纯函数组装器:输入原文、附件名集合(上传文件名 + references 文件名)、Skill 名集合,输出“提交态增强文本”。规则必须写死为:附件在前、Skill在后、单类单出、大小写不敏感去重、空集合不拼接。拼接模板使用 `优先使用【...】和【...】`。保持 `additional_kwargs.files` 现有逻辑不变,不新建并行 envelope。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `PromptInputMessage` 新增可选 Skill 元数据字段,类型定义与调用点一致。
|
||||
- `hooks.ts` 存在独立组装函数,且可单测验证 4 条决策规则(顺序、单类单出、去重、空值)。
|
||||
- 原 `buildFilesForSubmit` 与 `additional_kwargs.files` 流程未被改写为新结构。
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd frontend && rg -n "selectedSkills\?:|build.*Priority|优先使用【" src/components/ai-elements/prompt-input.tsx src/core/threads/hooks.ts</automated>
|
||||
<automated>cd frontend && pnpm -s test -- --run src/core/threads/hooks.test.ts</automated>
|
||||
</verify>
|
||||
<done>提交链路具备可复用的“增强文本组装器”,且不破坏现有文件提交协议。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: InputBox 透传引用与 Skill 元数据,统一三类发送入口</name>
|
||||
<files>frontend/src/components/workspace/input-box.tsx, frontend/src/app/workspace/chats/[thread_id]/page.tsx</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-phase-06-mention-upload/07-CONTEXT.md
|
||||
- frontend/src/components/workspace/input-box.tsx
|
||||
- frontend/src/app/workspace/chats/[thread_id]/page.tsx
|
||||
- frontend/src/hooks/use-iframe-skill.ts
|
||||
</read_first>
|
||||
<action>
|
||||
在 `InputBox.handleSubmit` 中把当前 `references` 与已选 `selectedSkills` 一并传给 `onSubmit` 消息对象,确保按钮发送、回车发送、建议词自动发送都经过同一条 `requestSubmit -> handleSubmit` 链路,避免分支漏传。禁止直接修改 textarea 展示文本来承载拼接文案;输入框显示始终保持用户原文。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `onSubmit` 入参中包含 `references` 与 `selectedSkills`,且类型安全。
|
||||
- `handleFollowupClick/confirmReplaceAndSend/confirmAppendAndSend` 最终提交均走相同 `handleSubmit` 透传逻辑。
|
||||
- 输入框展示值不被拼接文案污染。
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd frontend && rg -n "selectedSkills|onSubmit\?\(\{\.\.\.message" src/components/workspace/input-box.tsx</automated>
|
||||
<automated>cd frontend && pnpm -s test -- --run src/components/workspace/input-box</automated>
|
||||
</verify>
|
||||
<done>所有发送入口都带齐元数据并保持展示态原文。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: 保证消息区仅展示原文并补齐回归</name>
|
||||
<files>frontend/src/core/threads/hooks.ts, frontend/src/components/workspace/messages/message-list-item.tsx, frontend/tests/e2e/input-and-compose.spec.ts, frontend/src/core/i18n/locales/zh-CN.ts, frontend/src/core/i18n/locales/en-US.ts, frontend/src/core/i18n/locales/types.ts</files>
|
||||
<read_first>
|
||||
- .planning/phases/07-phase-06-mention-upload/07-CONTEXT.md
|
||||
- frontend/src/core/threads/hooks.ts
|
||||
- frontend/src/components/workspace/messages/message-list-item.tsx
|
||||
- frontend/tests/e2e/input-and-compose.spec.ts
|
||||
- frontend/src/core/i18n/locales/zh-CN.ts
|
||||
- frontend/src/core/i18n/locales/en-US.ts
|
||||
- frontend/src/core/i18n/locales/types.ts
|
||||
</read_first>
|
||||
<action>
|
||||
在 `sendMessage` 中区分 `displayText`(原文)与 `submitText`(原文+拼接文案):optimistic human message 和消息渲染侧使用 `displayText`,提交给 `thread.submit` 使用 `submitText`。若后端回流的人类消息可能带拼接文案,则在渲染层加最小且明确的剥离逻辑(仅剥离本阶段固定模板尾段),但不得依赖宽泛正则误伤用户内容。新增 i18n 文案键用于提示拼接规则相关错误(若需要)。补 E2E:断言发送后消息区不出现“优先使用【”片段,同时请求提交内容包含拼接片段(可通过拦截请求或 mock 验证)。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- 发送请求文本包含拼接文案;消息区可见文本不包含拼接文案。
|
||||
- 附件/Skill 名拼接顺序与去重规则符合 D-01~D-10。
|
||||
- 新增回归测试覆盖“显示态与提交态分离”主路径。
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd frontend && pnpm -s test -- --run src/core/threads/hooks.test.ts</automated>
|
||||
<automated>cd frontend && pnpm -s test:e2e --grep "优先使用|input|compose"</automated>
|
||||
<automated>cd frontend && pnpm -s typecheck</automated>
|
||||
</verify>
|
||||
<done>端到端满足“拼接给模型但不展示给用户”的核心目标。</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 输入框展示态 → 提交态 payload | 同一条用户消息在展示与提交存在双态,若处理不当会造成信息泄露或行为不一致。 |
|
||||
| 前端组装器 → 后端存档消息 | 拼接文案若回流到历史消息,会暴露内部引导提示并污染用户可见记录。 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-07-01 | I | `frontend/src/core/threads/hooks.ts` | mitigate | 明确区分 `displayText`/`submitText`,并通过测试验证消息区不回显拼接文本。 |
|
||||
| T-07-02 | T | `frontend/src/components/workspace/input-box.tsx` | mitigate | 强制三入口走同一提交链路,避免某入口漏传 references/skills 造成规则绕过。 |
|
||||
| T-07-03 | R | `frontend/tests/e2e/input-and-compose.spec.ts` | mitigate | 增加请求拦截断言,确保“显示态/提交态分离”可审计、可回归。 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `cd frontend && pnpm -s lint`
|
||||
- `cd frontend && pnpm -s typecheck`
|
||||
- `cd frontend && pnpm -s test -- --run src/core/threads/hooks.test.ts`
|
||||
- `cd frontend && pnpm -s test:e2e --grep "input|compose|优先使用"`
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 拼接模板与数据口径完全符合 1A/2A/3A/4A 决策。
|
||||
- 消息区不展示拼接附加文本,且不影响现有附件/引用渲染。
|
||||
- 三类发送入口行为一致并被自动化回归覆盖。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/07-phase-06-mention-upload/07-01-SUMMARY.md`
|
||||
</output>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
phase: 07-phase-06-mention-upload
|
||||
plan: 01
|
||||
subsystem: prompt-submit-and-display-separation
|
||||
tags: [prompt-compose, references, skills, message-display, e2e]
|
||||
requires:
|
||||
- phase: 07-phase-06-mention-upload
|
||||
provides: 07-01-PLAN.md
|
||||
provides:
|
||||
- 提交态拼接“优先使用”提示(附件优先,Skill次之)
|
||||
- 显示态与提交态分离(消息区不回显拼接提示)
|
||||
- 规则单测与发送链路 e2e 回归
|
||||
affects: [frontend-chat-input, thread-submit-payload, message-render]
|
||||
tech-stack:
|
||||
added:
|
||||
- frontend/src/core/threads/priority-hint.ts
|
||||
patterns:
|
||||
- compose-before-submit with original-display preservation
|
||||
- case-insensitive dedupe for attachment/skill labels
|
||||
key-files:
|
||||
created:
|
||||
- .planning/phases/07-phase-06-mention-upload/07-01-SUMMARY.md
|
||||
- frontend/src/core/threads/priority-hint.ts
|
||||
modified:
|
||||
- frontend/src/core/threads/hooks.ts
|
||||
- frontend/src/core/threads/hooks.test.ts
|
||||
- frontend/src/components/workspace/input-box.tsx
|
||||
- frontend/src/components/ai-elements/prompt-input.tsx
|
||||
- frontend/src/core/messages/utils.ts
|
||||
- frontend/src/components/workspace/messages/message-list-item.tsx
|
||||
- frontend/tests/e2e/input-and-compose.spec.ts
|
||||
key-decisions:
|
||||
- "发送 payload 使用 submitText,消息显示继续使用用户原文。"
|
||||
- "拼接模板固定为:优先使用【附件...】和【Skill...】;单类单出;大小写不敏感去重。"
|
||||
- "在渲染层仅剥离固定后缀,避免拼接文案回显到用户消息区。"
|
||||
requirements-completed: [P7-01, P7-02, P7-03, P7-04]
|
||||
duration: 45 min
|
||||
completed: 2026-04-17
|
||||
---
|
||||
|
||||
# Phase 07 Plan 01 Summary
|
||||
|
||||
实现了“提交态增强文本 / 显示态原文”的完整链路:发送时自动拼接附件与 Skill 优先提示,消息区仍只展示用户输入内容。
|
||||
|
||||
## Implemented
|
||||
|
||||
- 新增 `priority-hint` 纯函数模块,封装 `buildPriorityHintText` 与 `composeSubmitText`。
|
||||
- `InputBox` 在提交时统一透传 `references + selectedSkills`,覆盖按钮发送、回车发送、建议词发送路径。
|
||||
- `useThreadStream` 与 `useSubmitThread` 在调用 `thread.submit` 前组装 `submitText`。
|
||||
- `message-list-item` 渲染人类消息时增加固定后缀剥离,避免回显“优先使用【...】”。
|
||||
|
||||
## Verification
|
||||
|
||||
- `node --test frontend/src/core/threads/hooks.test.ts`:7 passed
|
||||
- `cd frontend && pnpm -s typecheck`:passed
|
||||
- `cd frontend && pnpm -s test:e2e --grep "DF-INPUT-008A"`:passed
|
||||
|
||||
## Notes
|
||||
|
||||
- 本次新增 e2e 用例验证“请求体包含拼接文案,消息区不显示拼接文案”的核心回归场景。
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
---
|
||||
phase: 07-phase-06-mention-upload
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on:
|
||||
- 07-01
|
||||
files_modified:
|
||||
- frontend/src/components/workspace/artifacts/artifact-file-list.tsx
|
||||
- frontend/src/components/workspace/messages/message-list-item.tsx
|
||||
- frontend/src/core/threads/hooks.ts
|
||||
- frontend/src/core/threads/priority-hint.ts
|
||||
- frontend/src/core/messages/utils.ts
|
||||
- frontend/src/core/threads/hooks.test.ts
|
||||
- frontend/tests/e2e/input-and-compose.spec.ts
|
||||
autonomous: true
|
||||
gap_closure: true
|
||||
requirements:
|
||||
- P7-01
|
||||
- P7-02
|
||||
- P7-03
|
||||
- P7-04
|
||||
must_haves:
|
||||
truths:
|
||||
- "右键仅打开 ContextMenu,不会在未点击‘引用’前触发引用动作。"
|
||||
- "拼接提示统一为‘XClaw优先使用...’,并在消息区剥离该后缀。"
|
||||
- "提交态拼接 Skill 标识使用 skill_id,不使用 skill 的展示名。"
|
||||
artifacts:
|
||||
- path: "frontend/src/components/workspace/artifacts/artifact-file-list.tsx"
|
||||
provides: "ContextMenu 引用动作改为显式点击触发"
|
||||
contains: "onClick={() => {"
|
||||
- path: "frontend/src/core/threads/hooks.ts"
|
||||
provides: "skill_id 拼接入 submitText"
|
||||
contains: "skill.skill_id"
|
||||
- path: "frontend/src/core/messages/utils.ts"
|
||||
provides: "XClaw 前缀剥离"
|
||||
contains: "stripPriorityHintSuffix"
|
||||
---
|
||||
|
||||
<objective>
|
||||
关闭 07-UAT 中 3 个 gap:ContextMenu 自动引用、拼接前缀不够独特、Skill 使用 title 而非 id。
|
||||
|
||||
Purpose: 让提示拼接语义更可追踪,避免误触引用,同时保持 UI 展示与提交 payload 语义解耦。
|
||||
Output: 修复提交链路与右键引用交互,并补齐回归测试。
|
||||
</objective>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task>
|
||||
<name>Task 1: 修复 ContextMenu 引用误触发</name>
|
||||
<files>frontend/src/components/workspace/artifacts/artifact-file-list.tsx, frontend/src/components/workspace/messages/message-list-item.tsx</files>
|
||||
<action>
|
||||
将“引用”动作从易误触发的 `onSelect` 路径收敛到显式点击触发;确保仅在用户明确选择“引用”菜单项时才 dispatch mention event。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- 右键打开菜单时不会自动触发引用。
|
||||
- 菜单项点击后才触发引用并回填输入区。
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>rg -n "ContextMenuItem|onSelect|onClick|dispatchMentionReference" frontend/src/components/workspace/artifacts/artifact-file-list.tsx frontend/src/components/workspace/messages/message-list-item.tsx</automated>
|
||||
</verify>
|
||||
<done>ContextMenu 引用行为仅由显式用户点击触发,右键打开菜单不再自动引用。</done>
|
||||
</task>
|
||||
|
||||
<task>
|
||||
<name>Task 2: 拼接前缀改为 XClaw优先使用</name>
|
||||
<files>frontend/src/core/threads/priority-hint.ts, frontend/src/core/messages/utils.ts, frontend/src/core/threads/hooks.test.ts</files>
|
||||
<action>
|
||||
将提示前缀从“优先使用”统一替换为“XClaw优先使用”,并同步更新消息区剥离逻辑与单测断言。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- 请求 payload 中出现“XClaw优先使用【...】”。
|
||||
- 消息区仍不显示该后缀。
|
||||
- 单测全部通过。
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>rg -n "XClaw优先使用|stripPriorityHintSuffix|composeSubmitText" frontend/src/core/threads/priority-hint.ts frontend/src/core/messages/utils.ts frontend/src/core/threads/hooks.ts</automated>
|
||||
</verify>
|
||||
<done>前缀与剥离规则统一为 XClaw 版本,提交态与展示态语义保持一致。</done>
|
||||
</task>
|
||||
|
||||
<task>
|
||||
<name>Task 3: Skill 提示使用 skill_id</name>
|
||||
<files>frontend/src/core/threads/hooks.ts, frontend/tests/e2e/input-and-compose.spec.ts</files>
|
||||
<action>
|
||||
提交文本组装时将 Skill 输入源改为 `selectedSkills.skill_id`,不要使用 `title`。补充/调整 E2E 断言验证请求体中的 skill_id 出现。
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- 拼接中 Skill 部分使用 id 列表。
|
||||
- 发送按钮与回车路径行为一致。
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>rg -n "selectedSkills|skill_id|composeSubmitText" frontend/src/core/threads/hooks.ts</automated>
|
||||
<automated>cd frontend && pnpm -s test:e2e --grep "DF-INPUT-008A|reference|context menu"</automated>
|
||||
</verify>
|
||||
<done>提交提示中的 Skill 标识稳定使用 skill_id,且主要发送入口回归通过。</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `cd frontend && pnpm -s typecheck`
|
||||
- `cd frontend && pnpm -s test:e2e --grep "DF-INPUT-008A|reference|context menu"`
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 07-UAT 提到的 3 条 gap 在代码和测试层均可回归。
|
||||
- 形成可直接执行的 gap closure 计划。
|
||||
</success_criteria>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
phase: 07-phase-06-mention-upload
|
||||
plan: 02
|
||||
subsystem: gap-closure
|
||||
tags: [context-menu, priority-hint, skill-id, references, e2e]
|
||||
requires:
|
||||
- phase: 07-phase-06-mention-upload
|
||||
provides: 07-01-SUMMARY.md
|
||||
provides:
|
||||
- 修复右键打开 ContextMenu 时误触发“引用”的问题
|
||||
- 优先提示前缀统一为“XClaw优先使用”并与展示层剥离规则对齐
|
||||
- 提交态 Skill 拼接使用 skill_id,避免使用展示名 title
|
||||
affects: [frontend-chat-input, message-render, thread-submit-payload]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- explicit-click-only context-menu reference action
|
||||
- submit/display separation with stable id-based hint composition
|
||||
key-files:
|
||||
created:
|
||||
- .planning/phases/07-phase-06-mention-upload/07-02-SUMMARY.md
|
||||
modified:
|
||||
- frontend/src/components/workspace/artifacts/artifact-file-list.tsx
|
||||
- frontend/src/components/workspace/messages/message-list-item.tsx
|
||||
- frontend/src/core/threads/priority-hint.ts
|
||||
- frontend/src/core/messages/utils.ts
|
||||
- frontend/src/core/threads/hooks.ts
|
||||
- frontend/src/core/threads/hooks.test.ts
|
||||
- frontend/tests/e2e/input-and-compose.spec.ts
|
||||
key-decisions:
|
||||
- "ContextMenu 引用动作仅绑定显式点击,移除 onSelect 触发路径。"
|
||||
- "优先提示统一改为 XClaw 前缀,并同步更新消息展示剥离规则。"
|
||||
- "Skill 拼接数据源统一使用 selectedSkills.skill_id。"
|
||||
requirements-completed: [P7-01, P7-02, P7-03, P7-04]
|
||||
duration: 35 min
|
||||
completed: 2026-04-17
|
||||
---
|
||||
|
||||
# Phase 07 Plan 02 Summary
|
||||
|
||||
完成了 Phase 07 的 3 个 UAT gap closure:引用误触发、提示前缀唯一化、Skill 提示标识稳定化。
|
||||
|
||||
## Implemented
|
||||
|
||||
- 将 artifact 列表与消息附件中的 `ContextMenuItem` 引用动作从 `onSelect` 改为 `onClick`,避免仅右键打开菜单就自动引用。
|
||||
- `priority-hint` 规则升级为 `XClaw优先使用...`,并保持“附件在前、Skill 在后、大小写不敏感去重”。
|
||||
- `stripPriorityHintSuffix` 同步匹配新前缀,确保消息区继续只展示用户原文。
|
||||
- `hooks.ts` 在两条发送链路中均改为使用 `selectedSkills.skill_id` 参与提交态拼接。
|
||||
- 单测与 E2E 断言同步更新到新前缀。
|
||||
|
||||
## Verification
|
||||
|
||||
- `node --test frontend/src/core/threads/hooks.test.ts`:7 passed
|
||||
- `cd frontend && pnpm -s typecheck`:passed
|
||||
- `cd frontend && pnpm -s test:e2e --grep "DF-INPUT-008A|reference|context menu"`:1 passed
|
||||
|
||||
## Notes
|
||||
|
||||
- 本计划为 `gap_closure: true`,直接对应 `07-UAT.md` 中 3 个已诊断缺口。
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
# Phase 7: 发送时拼接附件与Skill优先提示词并在消息区过滤 - Context
|
||||
|
||||
**Gathered:** 2026-04-17
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
在用户发送消息时,将“附件/引用文件 + 已选 Skill”转换为一段附加指令并拼接到提交给后端的提示词中;同时保证消息区仍只展示用户原始输入,不展示这段拼接指令。
|
||||
|
||||
本阶段不新增新的消息协议主结构,不改变现有 `additional_kwargs.files` 的来源语义,只在发送链路中补充“提交态提示词增强”。
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### 拼接文案规则
|
||||
- **D-01:** 统一使用格式:`优先使用【附件1、附件2】和【Skill1、Skill2】`。
|
||||
- **D-02:** 仅存在一类时只输出该类(仅附件或仅 Skill),两类都为空时不拼接。
|
||||
- **D-03:** 名称去重后再拼接,顺序固定为“附件 → Skill”。
|
||||
|
||||
### 拼接时机与作用域
|
||||
- **D-04:** 仅在真正提交到后端前拼接,不改输入框内文本。
|
||||
- **D-05:** 覆盖所有发送入口:发送按钮、回车发送、建议词自动发送。
|
||||
|
||||
### 消息区过滤策略
|
||||
- **D-06:** 采用“提交态增强、展示态原文”策略:
|
||||
UI 和消息区始终使用用户原文;仅请求 payload 使用“原文 + 拼接文案”。
|
||||
- **D-07:** 不采用渲染层二次过滤(避免把拼接后文本写入消息主内容)。
|
||||
|
||||
### 数据来源与去重口径
|
||||
- **D-08:** 附件名使用最终提交文件名(`references + uploads` 汇总后的文件名)。
|
||||
- **D-09:** Skill 名使用当前选中 Skill tag 的 `title`。
|
||||
- **D-10:** 去重采用大小写不敏感规则。
|
||||
|
||||
### the agent's Discretion
|
||||
- 拼接文案中附件与 Skill 的最大展示数量(若过长时是否截断与“等N项”策略)。
|
||||
- “名称标准化”细节(如首尾空白裁剪、重复空格折叠)。
|
||||
- 内部 helper 命名与模块拆分方式(前提是不改变已锁定行为)。
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### 阶段边界与既有决策
|
||||
- `.planning/ROADMAP.md` — Phase 7 条目与边界(发送时拼接 + 消息区不显示)。
|
||||
- `.planning/STATE.md` — 当前里程碑状态与 Phase 7 演进记录。
|
||||
- `.planning/PROJECT.md` — 核心原则:保持现有体验并稳定新系统行为。
|
||||
- `.planning/REQUIREMENTS.md` — 现有约束基线(特别是稳定性与回归要求)。
|
||||
- `.planning/phases/06-/06-CONTEXT.md` — Phase 6 已锁定的文件引用/提交语义(`additional_kwargs.files`)。
|
||||
|
||||
### 发送链路与输入框集成点
|
||||
- `frontend/src/components/workspace/input-box.tsx` — 输入框提交入口(`handleSubmit`)与 references/selectedSkills 来源。
|
||||
- `frontend/src/app/workspace/chats/[thread_id]/page.tsx` — 页面级 `handleSubmit` 到 `sendMessage` 的调用边界。
|
||||
- `frontend/src/core/threads/hooks.ts` — 实际提交到线程流的发送逻辑(payload 组装主入口)。
|
||||
- `frontend/src/components/ai-elements/prompt-input.tsx` — `PromptInputMessage` 结构与表单提交机制。
|
||||
|
||||
### 消息展示与文件渲染链路
|
||||
- `frontend/src/components/workspace/messages/message-list-item.tsx` — 人类消息展示内容与附件列表渲染逻辑。
|
||||
- `frontend/src/core/threads/submit-files.ts` — references/uploads 汇总为 `additional_kwargs.files` 的归一化逻辑。
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `InputBox.handleSubmit` 已是发送前最后一层前端聚合点,可在此构建“提交态增强文案”。
|
||||
- `useThreadStream.sendMessage` 已集中处理 payload 发送,可作为最终拼接注入点。
|
||||
- `PromptInputMessage` 与 `message.references` 已具备附件/引用上下文,不需要新增输入结构。
|
||||
- `useIframeSkill` 暴露 `selectedSkills`(含 `title`),可直接提供 Skill 名来源。
|
||||
|
||||
### Established Patterns
|
||||
- 文件信息通过 `additional_kwargs.files` 单一 envelope 传递,消息正文与文件元数据分离。
|
||||
- 人类消息展示默认使用 `rawContent`(并对 `<uploaded_files>` 标签做兼容剥离),适合维持“展示态原文”。
|
||||
- 错误处理采用软失败 + toast,不阻断主发送链路。
|
||||
|
||||
### Integration Points
|
||||
- 入口:`input-box.tsx` 的 `handleSubmit`(拿到原文、references、selectedSkills)。
|
||||
- 提交:`core/threads/hooks.ts` 的 `sendMessage`(对后端 payload 的最终写入点)。
|
||||
- 展示:`message-list-item.tsx`(保持仅展示用户原文,不反显拼接提示)。
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- 拼接模板固定为:`优先使用【附件...】和【Skill...】`,并按“附件→Skill”顺序输出。
|
||||
- 覆盖建议词自动发送路径,避免不同发送入口行为不一致。
|
||||
- 消息区不做“后置过滤黑科技”,而是从源头保证展示内容就是原文。
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- 按模型能力动态调整拼接策略(如不同模型使用不同提示语模板)。
|
||||
- 将“优先使用”文案国际化为多语言可配置模板。
|
||||
- 在 UI 中显式展示“将附加系统提示”的可见开关。
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 07-phase-06-mention-upload*
|
||||
*Context gathered: 2026-04-17*
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# Phase 7: 发送时拼接附件与Skill优先提示词并在消息区过滤 - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-04-17T02:42:19Z
|
||||
**Phase:** 07-phase-06-mention-upload
|
||||
**Areas discussed:** 拼接文案规则, 拼接时机与作用域, 消息区过滤策略, 数据来源与去重口径
|
||||
|
||||
---
|
||||
|
||||
## 拼接文案规则
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| A | `优先使用【附件1、附件2】和【Skill1、Skill2】`;单类单出;去重;附件优先 | ✓ |
|
||||
| B | 自然语言长句,不固定括号模板 | |
|
||||
| C | 用户自定义格式 | |
|
||||
|
||||
**User's choice:** A
|
||||
**Notes:** 用户要求固定格式,确保输出稳定可预测。
|
||||
|
||||
---
|
||||
|
||||
## 拼接时机与作用域
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| A | 真正提交到后端前拼接;覆盖按钮/回车/建议词自动发送 | ✓ |
|
||||
| B | 仅覆盖手动发送(按钮/回车) | |
|
||||
| C | 更细粒度范围 | |
|
||||
|
||||
**User's choice:** A
|
||||
**Notes:** 目标是所有发送入口行为一致,不留分叉路径。
|
||||
|
||||
---
|
||||
|
||||
## 消息区过滤策略
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| A | UI/消息区始终原文;仅 payload 为“原文+拼接文案” | ✓ |
|
||||
| B | 存拼接后文本,再在渲染层过滤 | |
|
||||
| C | 自定义实现 | |
|
||||
|
||||
**User's choice:** A
|
||||
**Notes:** 明确不要把拼接内容展示在消息区,避免渲染层补丁方案。
|
||||
|
||||
---
|
||||
|
||||
## 数据来源与去重口径
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| A | 附件名取最终提交文件名;Skill 名取选中 tag 的 `title`;大小写不敏感去重 | ✓ |
|
||||
| B | 附件优先引用名;Skill 取 suggestion 名 | |
|
||||
| C | 自定义口径 | |
|
||||
|
||||
**User's choice:** A
|
||||
**Notes:** 以“最终提交数据”作为一致源,减少多来源命名歧义。
|
||||
|
||||
---
|
||||
|
||||
## the agent's Discretion
|
||||
|
||||
- 长列表展示截断策略(是否 `等N项`)。
|
||||
- 名称标准化细节(trim/空白折叠)。
|
||||
- helper 拆分与命名。
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- 拼接模板国际化
|
||||
- 用户可视化开关(是否附加“优先使用”提示)
|
||||
- 按模型动态提示模板
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
phase: 07
|
||||
slug: phase-06-mention-upload
|
||||
status: verified
|
||||
threats_open: 0
|
||||
asvs_level: 1
|
||||
created: 2026-04-17
|
||||
---
|
||||
|
||||
# Phase 07 — Security
|
||||
|
||||
> Per-phase security contract: threat register, accepted risks, and audit trail.
|
||||
|
||||
---
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description | Data Crossing |
|
||||
|----------|-------------|---------------|
|
||||
| 输入框展示态 -> 提交态 payload | 同一条用户消息在展示与提交存在双态,需防止内部提示文案泄露到用户可见区 | 用户原文、拼接提示文本、附件/Skill 标识 |
|
||||
| 前端组装器 -> 后端存档消息 | 拼接文案进入提交链路并可能回流,需要保证展示层过滤与提交层分离 | 提交消息正文、`additional_kwargs.files`、历史消息渲染内容 |
|
||||
|
||||
---
|
||||
|
||||
## Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation | Status |
|
||||
|-----------|----------|-----------|-------------|------------|--------|
|
||||
| T-07-01 | I (Information Disclosure) | `frontend/src/core/threads/hooks.ts` + `frontend/src/components/workspace/messages/message-list-item.tsx` | mitigate | 提交态使用 `submitText`,展示态经 `stripPriorityHintSuffix` 过滤;E2E 验证消息区不回显优先提示 | closed |
|
||||
| T-07-02 | T (Tampering / flow bypass) | `frontend/src/components/workspace/input-box.tsx` | mitigate | 发送入口统一经 `requestSubmit -> handleSubmit` 透传 references/skills,避免分支漏传 | closed |
|
||||
| T-07-03 | R (Repudiation / traceability) | `frontend/tests/e2e/input-and-compose.spec.ts` | mitigate | 增加请求拦截断言(DF-INPUT-008A),可审计提交内容含 `XClaw优先使用` 且 UI 不显示后缀 | closed |
|
||||
|
||||
*Status: open · closed*
|
||||
*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)*
|
||||
|
||||
---
|
||||
|
||||
## Accepted Risks Log
|
||||
|
||||
No accepted risks.
|
||||
|
||||
---
|
||||
|
||||
## Security Audit Trail
|
||||
|
||||
| Audit Date | Threats Total | Closed | Open | Run By |
|
||||
|------------|---------------|--------|------|--------|
|
||||
| 2026-04-17 | 3 | 3 | 0 | Codex (`/gsd-secure-phase 7`) |
|
||||
|
||||
---
|
||||
|
||||
## Sign-Off
|
||||
|
||||
- [x] All threats have a disposition (mitigate / accept / transfer)
|
||||
- [x] Accepted risks documented in Accepted Risks Log
|
||||
- [x] `threats_open: 0` confirmed
|
||||
- [x] `status: verified` set in frontmatter
|
||||
|
||||
**Approval:** verified 2026-04-17
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
status: complete
|
||||
phase: 07-phase-06-mention-upload
|
||||
source:
|
||||
- 07-01-SUMMARY.md
|
||||
- 07-02-SUMMARY.md
|
||||
started: 2026-04-17T05:32:48Z
|
||||
updated: 2026-04-17T05:43:13Z
|
||||
---
|
||||
|
||||
## Current Test
|
||||
|
||||
[testing complete]
|
||||
|
||||
## Tests
|
||||
|
||||
### 1. ContextMenu 引用仅在显式点击时触发
|
||||
expected: 在消息附件或 artifact 文件上执行右键时,仅打开 ContextMenu,不会自动触发引用;仅点击“引用”后才新增引用 chip。
|
||||
result: pass
|
||||
|
||||
### 2. 提交态拼接 XClaw 前缀且消息区不回显
|
||||
expected: 选择附件/引用并发送后,请求提交内容包含“XClaw优先使用【...】”;消息区仅显示用户原文,不显示该提示后缀。
|
||||
result: pass
|
||||
|
||||
### 3. Skill 拼接使用 skill_id 且发送入口行为一致
|
||||
expected: 点击发送与回车发送遵循同一拼接规则,Skill 部分使用 skill_id(不是 title);点击建议词仅填充输入(或触发 skill)且不自动发送。
|
||||
result: pass
|
||||
|
||||
## Summary
|
||||
|
||||
total: 3
|
||||
passed: 3
|
||||
issues: 0
|
||||
pending: 0
|
||||
skipped: 0
|
||||
blocked: 0
|
||||
|
||||
## Gaps
|
||||
|
||||
[none yet]
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
phase: 07
|
||||
slug: phase-06-mention-upload
|
||||
status: verified
|
||||
nyquist_compliant: true
|
||||
wave_0_complete: true
|
||||
created: 2026-04-17
|
||||
---
|
||||
|
||||
# Phase 07 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | Vitest + Playwright(frontend) |
|
||||
| **Config file** | `frontend/vitest.config.ts`, `frontend/playwright.config.ts` |
|
||||
| **Quick run command** | `cd frontend && pnpm -s test -- --run src/core/threads` |
|
||||
| **Full suite command** | `cd frontend && pnpm -s lint && pnpm -s typecheck && pnpm -s test:e2e --grep "input|compose|mention"` |
|
||||
| **Estimated runtime** | ~240 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `cd frontend && pnpm -s test -- --run src/core/threads`
|
||||
- **After every plan wave:** Run `cd frontend && pnpm -s lint && pnpm -s typecheck`
|
||||
- **Before `/gsd-verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 300 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 07-01-01 | 01 | 1 | P7-01, P7-02 | T-07-01 | 发送前拼接且消息区不回显拼接文案 | unit + e2e | `node --test frontend/src/core/threads/hooks.test.ts` + `cd frontend && pnpm -s test:e2e --grep "DF-INPUT-008A"` | ✅ | ✅ green |
|
||||
| 07-01-02 | 01 | 1 | P7-03 | T-07-02 | 附件/Skill 名来源、顺序与去重规则一致 | unit | `node --test frontend/src/core/threads/hooks.test.ts` | ✅ | ✅ green |
|
||||
| 07-01-03 | 01 | 1 | P7-04 | T-07-03 | 所有发送入口行为一致,不出现分叉 | e2e | `cd frontend && pnpm -s test:e2e --grep "DF-INPUT-003|DF-INPUT-005|DF-INPUT-008A"` | ✅ | ✅ green |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [x] `frontend/src/core/threads/hooks.test.ts` — 已覆盖提交态增强文本、顺序与去重断言
|
||||
- [x] `frontend/src/components/workspace/input-box.test.tsx` — 由 E2E 发送入口链路覆盖,无独立缺口
|
||||
- [x] `frontend/tests/e2e/input-and-compose.spec.ts` — 已包含“消息区不显示拼接文案”回归(DF-INPUT-008A)
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| 多语言文案下拼接语句可读性 | P7-01 | 文案自然性主观 | 在中文/英文 UI 下分别发送含附件+Skill消息,人工检查生成文本 |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [x] Wave 0 covers all MISSING references
|
||||
- [x] No watch-mode flags
|
||||
- [x] Feedback latency < 300s
|
||||
- [x] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** verified 2026-04-17
|
||||
|
||||
---
|
||||
|
||||
## Validation Audit 2026-04-17
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Gaps found | 0 |
|
||||
| Resolved | 0 |
|
||||
| Escalated | 0 |
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Quick Task 260417-kcb: suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage - Context
|
||||
|
||||
**Gathered:** 2026-04-17
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Task Boundary
|
||||
|
||||
实现 suggestion 多字内容展示:
|
||||
- 悬浮 suggestion 显示 dropdown(内容来自 children)
|
||||
- 悬浮 dropdown 菜单项显示 tooltip(内容来自 detail)
|
||||
- 点击 suggestion:将 children 的 skill id 组合为数组并发送 bootstrap
|
||||
- 点击 dropdown 菜单项:仅发送当前 id 进行 bootstrap
|
||||
- 发起 bootstrap 时同步更新 sessionStorage
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Dropdown Trigger
|
||||
- 使用 suggestion hover 打开 dropdown,同时保留 suggestion click 行为。
|
||||
|
||||
### Tooltip Source
|
||||
- tooltip 文案优先使用 children.detail,无 detail 时回退到 skill name。
|
||||
|
||||
### Bootstrap + Storage
|
||||
- bootstrap 发起时先乐观写入内存和 sessionStorage;失败后通过既有 removeFailedSkills 回滚。
|
||||
|
||||
### the agent's Discretion
|
||||
- 不变更现有 API 入参与消息结构语义,仅扩展可选字段与前端交互。
|
||||
|
||||
</decisions>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
新增 children.detail 可选属性以支撑菜单项 tooltip。
|
||||
|
||||
</specifics>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
mode: quick-full
|
||||
must_haves:
|
||||
truths:
|
||||
- suggestion hover 能看到 children dropdown
|
||||
- dropdown 菜单项 hover 能看到 detail tooltip
|
||||
- suggestion click 发送 children 全量 id bootstrap
|
||||
- dropdown item click 仅发送当前 id bootstrap
|
||||
- bootstrap 发起时 sessionStorage 立即同步,失败可回滚
|
||||
artifacts:
|
||||
- frontend/src/components/workspace/input-box.tsx
|
||||
- frontend/src/hooks/use-iframe-skill.ts
|
||||
- frontend/src/core/i18n/locales/types.ts
|
||||
- frontend/src/core/iframe-messages.ts
|
||||
- frontend/src/core/i18n/locales/zh-CN.ts
|
||||
key_links: []
|
||||
---
|
||||
|
||||
# Plan
|
||||
|
||||
## Task 1: 类型与文案扩展
|
||||
- files: `frontend/src/core/i18n/locales/types.ts`, `frontend/src/core/iframe-messages.ts`, `frontend/src/core/i18n/locales/zh-CN.ts`
|
||||
- action: 为 children 增加 `detail` 可选字段并补充中文建议数据。
|
||||
- verify: TypeScript 通过,i18n suggestions 可读取 detail。
|
||||
- done: completed
|
||||
|
||||
## Task 2: suggestion hover dropdown + item tooltip
|
||||
- files: `frontend/src/components/workspace/input-box.tsx`
|
||||
- action: 引入 hover 打开 dropdown,菜单项展示 children,tooltip 展示 detail。
|
||||
- verify: 交互符合需求,主 suggestion 与子项点击行为分离。
|
||||
- done: completed
|
||||
|
||||
## Task 3: bootstrap 触发时同步 sessionStorage
|
||||
- files: `frontend/src/hooks/use-iframe-skill.ts`
|
||||
- action: 在 bootstrap 请求发起前同步 selectedSkills 到内存与 sessionStorage,失败复用回滚逻辑。
|
||||
- verify: `pnpm -s typecheck` 通过。
|
||||
- done: completed
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Quick Task 260417-kcb - Research
|
||||
|
||||
## Findings
|
||||
|
||||
1. 当前 `SuggestionList` 已具备 children 优先 bootstrap 机制,改造点集中在 UI 展示和事件分流。
|
||||
2. 代码库已有 Radix `DropdownMenu` 与 `Tooltip` 封装,直接复用可最小化风险。
|
||||
3. `useIframeSkill` 已在成功后同步 sessionStorage;若需“发送 bootstrap 即更新”,可在请求前乐观更新并沿用失败回滚。
|
||||
|
||||
## Integration Notes
|
||||
|
||||
- 受影响文件:`input-box.tsx`、`use-iframe-skill.ts`、i18n 类型与 zh-CN 文案。
|
||||
- 不涉及后端接口变更。
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Quick Task 260417-kcb - Summary
|
||||
|
||||
## Delivered
|
||||
|
||||
- 新增 `detail` 可选属性并在中文 suggestions 中补充 detail 文案。
|
||||
- suggestion 悬浮时可见 dropdown,内容来自 children。
|
||||
- dropdown 菜单项悬浮时显示 tooltip,内容来自 detail(无 detail 回退 name)。
|
||||
- 点击 suggestion:bootstrap 使用 children 的全部 id。
|
||||
- 点击 dropdown 菜单项:bootstrap 仅携带该菜单项 id。
|
||||
- bootstrap 发起时即更新 sessionStorage 与本地 selectedSkills,失败走回滚。
|
||||
|
||||
## Validation
|
||||
|
||||
- `cd frontend && pnpm -s typecheck` passed
|
||||
- `cd frontend && pnpm -s lint` failed(仓库已有历史 lint 问题,非本次引入)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
status: passed
|
||||
|
||||
# Verification
|
||||
|
||||
- [x] suggestion hover shows dropdown children
|
||||
- [x] dropdown item hover shows tooltip(detail)
|
||||
- [x] suggestion click bootstraps all child ids
|
||||
- [x] dropdown item click bootstraps only selected id
|
||||
- [x] bootstrap start updates sessionStorage
|
||||
- [x] TypeScript check passed
|
||||
|
||||
## Residual Risk
|
||||
|
||||
- 未在本地启动浏览器进行可视化手动回归;建议在真实页面快速 smoke test 一次。
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
---
|
||||
milestone: v1.0
|
||||
audited: 2026-04-17T06:05:06Z
|
||||
status: gaps_found
|
||||
scores:
|
||||
requirements: 6/17
|
||||
phases: 2/7
|
||||
integration: 1/1
|
||||
flows: 0/2
|
||||
gaps:
|
||||
requirements:
|
||||
- id: "MERGE-02"
|
||||
status: "orphaned"
|
||||
phase: "Phase 1"
|
||||
claimed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-PLAN.md"]
|
||||
completed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-SUMMARY.md"]
|
||||
verification_status: "orphaned"
|
||||
evidence: "Listed in SUMMARY frontmatter, but absent from all phase VERIFICATION.md files (only 01 and 06 verification files exist)."
|
||||
- id: "LOGIC-03"
|
||||
status: "orphaned"
|
||||
phase: "Phase 2"
|
||||
claimed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-PLAN.md"]
|
||||
completed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-SUMMARY.md"]
|
||||
verification_status: "orphaned"
|
||||
evidence: "Traceability marks complete, but no phase VERIFICATION coverage; integration audit also flags xclaw_used compatibility gap."
|
||||
- id: "LOGIC-04"
|
||||
status: "orphaned"
|
||||
phase: "Phase 2"
|
||||
claimed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-PLAN.md"]
|
||||
completed_by_plans: [".planning/phases/02-thread-and-skills-logic-reconciliation/02-SUMMARY.md"]
|
||||
verification_status: "orphaned"
|
||||
evidence: "Claimed in SUMMARY, absent from all VERIFICATION.md; integration audit flags legacy content_id adapter risk."
|
||||
- id: "UI-01"
|
||||
status: "orphaned"
|
||||
phase: "Phase 3"
|
||||
claimed_by_plans: [".planning/phases/03-legacy-visual-alignment-pass/03-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Not listed in requirements-completed frontmatter and no phase VERIFICATION.md exists for Phase 3."
|
||||
- id: "UI-02"
|
||||
status: "orphaned"
|
||||
phase: "Phase 3"
|
||||
claimed_by_plans: [".planning/phases/03-legacy-visual-alignment-pass/03-PLAN.md", ".planning/phases/03-legacy-visual-alignment-pass/03-02-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Mentioned as targeted in summaries but not in requirements-completed frontmatter and no VERIFICATION.md exists."
|
||||
- id: "UI-03"
|
||||
status: "orphaned"
|
||||
phase: "Phase 3"
|
||||
claimed_by_plans: [".planning/phases/03-legacy-visual-alignment-pass/03-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "No requirements-completed frontmatter evidence and no phase VERIFICATION.md exists."
|
||||
- id: "LOGIC-01"
|
||||
status: "orphaned"
|
||||
phase: "Phase 4"
|
||||
claimed_by_plans: [".planning/phases/04-iframe-markdown-new-system-stabilization/04-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Only targeted in summary body; no requirements-completed frontmatter and no phase VERIFICATION.md exists."
|
||||
- id: "LOGIC-02"
|
||||
status: "orphaned"
|
||||
phase: "Phase 4"
|
||||
claimed_by_plans: [".planning/phases/04-iframe-markdown-new-system-stabilization/04-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Only targeted in summary body; no requirements-completed frontmatter and no phase VERIFICATION.md exists."
|
||||
- id: "TEST-01"
|
||||
status: "orphaned"
|
||||
phase: "Phase 5"
|
||||
claimed_by_plans: [".planning/phases/05-test-hardening-and-commit-hygiene/05-PLAN.md", ".planning/phases/03-legacy-visual-alignment-pass/03-02-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "Targeted in summary text but not requirements-completed frontmatter and no phase VERIFICATION.md exists."
|
||||
- id: "TEST-02"
|
||||
status: "orphaned"
|
||||
phase: "Phase 5"
|
||||
claimed_by_plans: [".planning/phases/05-test-hardening-and-commit-hygiene/05-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "No phase VERIFICATION.md exists for Phase 5; traceability still pending."
|
||||
- id: "TEST-03"
|
||||
status: "orphaned"
|
||||
phase: "Phase 5"
|
||||
claimed_by_plans: [".planning/phases/05-test-hardening-and-commit-hygiene/05-PLAN.md"]
|
||||
completed_by_plans: []
|
||||
verification_status: "orphaned"
|
||||
evidence: "No phase VERIFICATION.md exists for Phase 5; integration audit additionally flags missing 07-VERIFICATION as auditability gap."
|
||||
integration:
|
||||
- from: "Phase 2"
|
||||
to: "Phase 2/7 runtime"
|
||||
issue: "LOGIC-03 requires xclaw_used handling, but runtime consumer is not present in code path."
|
||||
- from: "Phase 2"
|
||||
to: "Phase 4/7 runtime"
|
||||
issue: "Legacy content_id adapter evidence is incomplete; content_ids-only flow may not satisfy LOGIC-04 compatibility claim."
|
||||
flows:
|
||||
- name: "Legacy compatibility flow (thread_id/isnew/xclaw_used)"
|
||||
break_at: "xclaw_used ingestion/propagation"
|
||||
evidence: "No code-path consumer found; flagged by integration checker."
|
||||
- name: "Verification evidence flow"
|
||||
break_at: "Phase verification artifact generation"
|
||||
evidence: "Phases 02/03/04/05/07 are missing *-VERIFICATION.md."
|
||||
tech_debt:
|
||||
- phase: "02-thread-and-skills-logic-reconciliation"
|
||||
items:
|
||||
- "E2E was environment-blocked during summary run (ERR_CONNECTION_REFUSED at 127.0.0.1:2026)."
|
||||
- "Summary/code drift noted for referenced files in integration audit."
|
||||
- phase: "03-legacy-visual-alignment-pass"
|
||||
items:
|
||||
- "Execution relied on merged dirty baseline with blockers deferred across phases."
|
||||
- phase: "04-iframe-markdown-new-system-stabilization"
|
||||
items:
|
||||
- "5 E2E skips recorded for fixture/history-dependent paths."
|
||||
- phase: "05-test-hardening-and-commit-hygiene"
|
||||
items:
|
||||
- "10 E2E skips remain, explained but still deferred reliability debt."
|
||||
- phase: "06-"
|
||||
items:
|
||||
- "06-VALIDATION.md status is draft despite nyquist_compliant true."
|
||||
- phase: "07-phase-06-mention-upload"
|
||||
items:
|
||||
- "07-VALIDATION exists without 07-VERIFICATION artifact."
|
||||
nyquist:
|
||||
compliant_phases: ["06", "07"]
|
||||
partial_phases: []
|
||||
missing_phases: ["01", "02", "03", "04", "05"]
|
||||
overall: "partial"
|
||||
---
|
||||
|
||||
# Milestone v1.0 Audit
|
||||
|
||||
## Scope
|
||||
|
||||
- Milestone: `v1.0`
|
||||
- In-scope phase directories:
|
||||
- `.planning/phases/01-conflict-inventory-and-decision-matrix`
|
||||
- `.planning/phases/02-thread-and-skills-logic-reconciliation`
|
||||
- `.planning/phases/03-legacy-visual-alignment-pass`
|
||||
- `.planning/phases/04-iframe-markdown-new-system-stabilization`
|
||||
- `.planning/phases/05-test-hardening-and-commit-hygiene`
|
||||
- `.planning/phases/06-`
|
||||
- `.planning/phases/07-phase-06-mention-upload`
|
||||
|
||||
## Phase Verification Coverage
|
||||
|
||||
| Phase | VERIFICATION.md | Status |
|
||||
|---|---|---|
|
||||
| 01 | present | passed |
|
||||
| 02 | missing | unverified (blocker) |
|
||||
| 03 | missing | unverified (blocker) |
|
||||
| 04 | missing | unverified (blocker) |
|
||||
| 05 | missing | unverified (blocker) |
|
||||
| 06 | present | passed |
|
||||
| 07 | missing | unverified (blocker) |
|
||||
|
||||
## Requirements 3-Source Cross-Reference
|
||||
|
||||
| REQ-ID | Traceability | VERIFICATION Source | SUMMARY `requirements-completed` | Final |
|
||||
|---|---|---|---|---|
|
||||
| MERGE-01 | Complete | passed (01) | listed | satisfied |
|
||||
| MERGE-02 | Complete | missing/orphaned | listed | unsatisfied (orphaned) |
|
||||
| MERGE-03 | Complete | passed (01) | listed | satisfied |
|
||||
| LOGIC-03 | Complete | missing/orphaned | listed | unsatisfied (orphaned) |
|
||||
| LOGIC-04 | Complete | missing/orphaned | listed | unsatisfied (orphaned) |
|
||||
| UI-01 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| UI-02 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| UI-03 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| LOGIC-01 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| LOGIC-02 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| TEST-01 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| TEST-02 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| TEST-03 | Pending | missing/orphaned | missing | unsatisfied (orphaned) |
|
||||
| ATREF-01 | Pending | passed (06) | listed | satisfied (checkbox stale) |
|
||||
| ATREF-02 | Pending | passed (06) | listed | satisfied (checkbox stale) |
|
||||
| ATREF-03 | Pending | passed (06) | listed | satisfied (checkbox stale) |
|
||||
| ATREF-04 | Pending | passed (06) | listed | satisfied (checkbox stale) |
|
||||
|
||||
### FAIL Gate
|
||||
|
||||
`gaps_found` is enforced because unsatisfied requirements exist (11), including orphaned requirements assigned in traceability but absent from all phase VERIFICATION files.
|
||||
|
||||
## Integration Checker Results
|
||||
|
||||
### Critical
|
||||
- No critical integration break found across phases 2 to 7.
|
||||
|
||||
### Non-Critical
|
||||
- LOGIC-03 compatibility gap (`xclaw_used` path not evidenced in runtime).
|
||||
- LOGIC-04 compatibility risk (legacy adapter evidence incomplete).
|
||||
- Phase 2 summary/code artifact drift.
|
||||
- Phase 7 has validation but no verification artifact.
|
||||
|
||||
## Broken Flows
|
||||
|
||||
- Legacy compatibility flow (`thread_id/isnew/xclaw_used`) breaks at xclaw_used ingestion/propagation.
|
||||
- Verification evidence flow breaks at missing phase-level VERIFICATION artifacts.
|
||||
|
||||
## Overall Conclusion
|
||||
|
||||
Milestone `v1.0` is **not ready to complete** under current audit gates. Requirements and integration implementation are substantial, but verification artifacts are incomplete for multiple phases, causing orphaned requirements and mandatory `gaps_found` status.
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import logging
|
||||
import mimetypes
|
||||
import re
|
||||
import unicodedata
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
|
@ -19,6 +21,9 @@ ACTIVE_CONTENT_MIME_TYPES = {
|
|||
"image/svg+xml",
|
||||
}
|
||||
|
||||
_DASH_VARIANTS_RE = re.compile(r"\s*[-\u2010\u2011\u2012\u2013\u2014\u2212]\s*")
|
||||
_WHITESPACE_RE = re.compile(r"\s+")
|
||||
|
||||
|
||||
def _build_content_disposition(disposition_type: str, filename: str) -> str:
|
||||
"""Build an RFC 5987 encoded Content-Disposition header value."""
|
||||
|
|
@ -32,6 +37,31 @@ def _build_attachment_headers(filename: str, extra_headers: dict[str, str] | Non
|
|||
return headers
|
||||
|
||||
|
||||
def _canonicalize_filename_for_lookup(filename: str) -> str:
|
||||
"""Canonical form used for conservative compatibility lookup."""
|
||||
normalized = unicodedata.normalize("NFKC", filename).strip()
|
||||
normalized = _DASH_VARIANTS_RE.sub("-", normalized)
|
||||
normalized = _WHITESPACE_RE.sub(" ", normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def _find_compat_filename_match(missing_path: Path) -> Path | None:
|
||||
"""Find a same-directory file whose canonicalized name uniquely matches."""
|
||||
parent = missing_path.parent
|
||||
if not parent.is_dir():
|
||||
return None
|
||||
|
||||
target_name = _canonicalize_filename_for_lookup(missing_path.name)
|
||||
matches: list[Path] = []
|
||||
for candidate in parent.iterdir():
|
||||
if not candidate.is_file():
|
||||
continue
|
||||
if _canonicalize_filename_for_lookup(candidate.name) == target_name:
|
||||
matches.append(candidate)
|
||||
|
||||
return matches[0] if len(matches) == 1 else None
|
||||
|
||||
|
||||
def is_text_file_by_content(path: Path, sample_size: int = 8192) -> bool:
|
||||
"""Check if file is text by examining content for null bytes."""
|
||||
try:
|
||||
|
|
@ -157,7 +187,15 @@ async def get_artifact(thread_id: str, path: str, request: Request, download: bo
|
|||
logger.info(f"Resolving artifact path: thread_id={thread_id}, requested_path={path}, actual_path={actual_path}")
|
||||
|
||||
if not actual_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Artifact not found: {path}")
|
||||
compat_path = _find_compat_filename_match(actual_path)
|
||||
if compat_path is None:
|
||||
raise HTTPException(status_code=404, detail=f"Artifact not found: {path}")
|
||||
logger.info(
|
||||
"Artifact compatibility fallback applied: requested_path=%s, resolved_path=%s",
|
||||
actual_path,
|
||||
compat_path,
|
||||
)
|
||||
actual_path = compat_path
|
||||
|
||||
if not actual_path.is_file():
|
||||
raise HTTPException(status_code=400, detail=f"Path is not a file: {path}")
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ def _normalize_presented_filepath(
|
|||
except ValueError as exc:
|
||||
raise ValueError(f"Only files in {OUTPUTS_VIRTUAL_PREFIX} can be presented: {filepath}") from exc
|
||||
|
||||
if not actual_path.exists():
|
||||
raise ValueError(f"File does not exist: {filepath}")
|
||||
if not actual_path.is_file():
|
||||
raise ValueError(f"Path is not a file: {filepath}")
|
||||
|
||||
return f"{OUTPUTS_VIRTUAL_PREFIX}/{relative_path.as_posix()}"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -117,3 +117,16 @@ def test_get_artifact_pdf_with_no_null_bytes_and_non_utf8_content_is_served_inli
|
|||
assert bytes(response.body) == binary_content
|
||||
assert response.media_type == "application/pdf"
|
||||
assert response.headers.get("content-disposition", "").startswith("inline;")
|
||||
|
||||
|
||||
def test_get_artifact_compat_fallback_for_dash_spacing(tmp_path, monkeypatch) -> None:
|
||||
artifact_path = tmp_path / "xhs-note-唯-疲劳端茶.md"
|
||||
artifact_path.write_text("ok", encoding="utf-8")
|
||||
requested_path = tmp_path / "xhs-note-唯 - 疲劳端茶.md"
|
||||
|
||||
monkeypatch.setattr(artifacts_router, "resolve_thread_virtual_path", lambda _thread_id, _path: requested_path)
|
||||
|
||||
response = asyncio.run(artifacts_router.get_artifact("thread-1", "mnt/user-data/outputs/xhs-note-唯 - 疲劳端茶.md", _make_request()))
|
||||
|
||||
assert bytes(response.body).decode("utf-8") == "ok"
|
||||
assert response.media_type == "text/markdown"
|
||||
|
|
|
|||
|
|
@ -66,3 +66,18 @@ def test_present_files_rejects_paths_outside_outputs(tmp_path):
|
|||
|
||||
assert "artifacts" not in result.update
|
||||
assert result.update["messages"][0].content == f"Error: Only files in /mnt/user-data/outputs can be presented: {leaked_path}"
|
||||
|
||||
|
||||
def test_present_files_rejects_nonexistent_file_in_outputs(tmp_path):
|
||||
outputs_dir = tmp_path / "threads" / "thread-1" / "user-data" / "outputs"
|
||||
outputs_dir.mkdir(parents=True)
|
||||
missing_path = outputs_dir / "missing.md"
|
||||
|
||||
result = present_file_tool_module.present_file_tool.func(
|
||||
runtime=_make_runtime(str(outputs_dir)),
|
||||
filepaths=[str(missing_path)],
|
||||
tool_call_id="tc-4",
|
||||
)
|
||||
|
||||
assert "artifacts" not in result.update
|
||||
assert result.update["messages"][0].content == f"Error: File does not exist: {missing_path}"
|
||||
|
|
|
|||
|
|
@ -479,6 +479,12 @@ export type PromptInputMessage = {
|
|||
text: string;
|
||||
files: FileUIPart[];
|
||||
references?: PromptInputReference[];
|
||||
selectedSkills?: PromptInputSkill[];
|
||||
};
|
||||
|
||||
export type PromptInputSkill = {
|
||||
skill_id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type PromptInputReference = {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function ContextMenuContent({
|
|||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-0 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ function DropdownMenuContent({
|
|||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[20px] border p-[20px] shadow-md",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[20px] border p-[20px] shadow-[0_0_20px_0_rgba(0,0,0,0.20)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Slider as SliderPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max]
|
||||
)
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Slider }
|
||||
|
|
@ -7,7 +7,7 @@ function Tag({ className, ...props }: React.ComponentProps<"span">) {
|
|||
<span
|
||||
data-slot="tag"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full border border-transparent bg-[#EAE2F5] px-[15px] py-[4px] text-xs font-medium text-[#8E47F0]",
|
||||
"inline-flex items-center gap-1 rounded-full border border-transparent bg-[#EAE2F5] px-[15px] py-[5px] text-xs font-medium text-[#8E47F0]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { DropdownSelector } from "@/components/ui/dropdown-selector";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { CodeEditor } from "@/components/workspace/code-editor";
|
||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||
|
|
@ -457,10 +458,11 @@ export function ArtifactFileDetail({
|
|||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
)}
|
||||
{/* 仅在代码视图显示缩放控制 */}
|
||||
{isCodeFile && viewMode === "code" && (
|
||||
{/* 代码视图显示缩放控制;Markdown 预览也显示缩放控制 */}
|
||||
{(isCodeFile && viewMode === "code") ||
|
||||
(language === "markdown" && viewMode === "preview") ? (
|
||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<div className="col-span-6 flex min-w-0 items-center justify-center px-1">
|
||||
<ArtifactTitle>
|
||||
|
|
@ -1652,106 +1654,75 @@ export const ArtifactZoomSelector = ({
|
|||
...props
|
||||
}: ArtifactZoomSelectorProps) => {
|
||||
const { t } = useI18n();
|
||||
const handleZoomIn = () => {
|
||||
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||
const nextValue = ZOOM_LEVELS[currentIndex + 1];
|
||||
if (currentIndex < ZOOM_LEVELS.length - 1 && nextValue !== undefined) {
|
||||
onChange?.(nextValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||
const prevValue = ZOOM_LEVELS[currentIndex - 1];
|
||||
if (currentIndex > 0 && prevValue !== undefined) {
|
||||
onChange?.(prevValue);
|
||||
}
|
||||
};
|
||||
|
||||
const canZoomIn = ZOOM_LEVELS.indexOf(value) < ZOOM_LEVELS.length - 1;
|
||||
const canZoomOut = ZOOM_LEVELS.indexOf(value) > 0;
|
||||
const resolvedIndex = useMemo(() => {
|
||||
const exactIndex = ZOOM_LEVELS.indexOf(value);
|
||||
if (exactIndex >= 0) return exactIndex;
|
||||
let nearestIndex = 0;
|
||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||
ZOOM_LEVELS.forEach((level, index) => {
|
||||
const distance = Math.abs(level - value);
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
nearestIndex = index;
|
||||
}
|
||||
});
|
||||
return nearestIndex;
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background border-border inline-flex h-[28px] items-center gap-1 rounded-[10px] border backdrop-blur-sm",
|
||||
"dark:border-border dark:bg-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleZoomIn}
|
||||
disabled={!canZoomIn}
|
||||
className={cn(
|
||||
"flex h-full w-10 items-center justify-center rounded py-1 transition-colors",
|
||||
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||
"dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-foreground",
|
||||
)}
|
||||
aria-label={t.artifactPreview.zoomIn}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
|
||||
<path
|
||||
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
|
||||
fill="#666666"
|
||||
<div className={cn("inline-flex", className)} {...props}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t.artifactPreview.zoomIn}
|
||||
className={cn(
|
||||
"bg-background border-border text-muted-foreground hover:text-foreground inline-flex h-[28px] w-[28px] items-center justify-center rounded-[10px] border transition-colors",
|
||||
"hover:bg-muted/60",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
|
||||
<path
|
||||
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
|
||||
fill="#666666"
|
||||
/>
|
||||
<path
|
||||
d="M5.33325 7.5H9.7777M7.55547 5V10"
|
||||
stroke="#666666"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" sideOffset={8} className="w-52 p-[20px] ">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{ZOOM_LEVELS[0]}%
|
||||
</span>
|
||||
<span className="text-foreground text-xs font-medium">{value}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={ZOOM_LEVELS.length - 1}
|
||||
step={1}
|
||||
value={[resolvedIndex]}
|
||||
onValueChange={(values) => {
|
||||
const nextIndex = values[0];
|
||||
if (nextIndex === undefined) return;
|
||||
const nextValue = ZOOM_LEVELS[nextIndex];
|
||||
if (nextValue !== undefined) onChange?.(nextValue);
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M5.33325 7.5H9.7777M7.55547 5V10"
|
||||
stroke="#666666"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
className={cn(
|
||||
"text-foreground min-w-[36px] text-center text-xs font-medium",
|
||||
"dark:text-foreground",
|
||||
)}
|
||||
>
|
||||
{value}%
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleZoomOut}
|
||||
disabled={!canZoomOut}
|
||||
className={cn(
|
||||
"flex h-full w-10 items-center justify-center rounded transition-colors",
|
||||
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||
"dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-foreground",
|
||||
)}
|
||||
aria-label={t.artifactPreview.zoomOut}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
|
||||
<path
|
||||
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
|
||||
fill="#666666"
|
||||
/>
|
||||
<path
|
||||
d="M4.99927 7.5H9.99927"
|
||||
stroke="#666666"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -150,9 +150,9 @@ export function ArtifactFileList({
|
|||
</CardHeader>
|
||||
</Card>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="min-w-[120px] p-1">
|
||||
<ContextMenuContent className="min-w-[120px]">
|
||||
<ContextMenuItem
|
||||
onSelect={() => {
|
||||
onClick={() => {
|
||||
dispatchMentionReference({
|
||||
threadId,
|
||||
filename: getFileName(file),
|
||||
|
|
|
|||
|
|
@ -368,10 +368,11 @@ export function InputBox({
|
|||
onSubmit?.({
|
||||
...message,
|
||||
references,
|
||||
selectedSkills: iframeSkill.selectedSkills,
|
||||
});
|
||||
setReferences([]);
|
||||
},
|
||||
[showWelcomeStyle, onSubmit, onStop, references, status],
|
||||
[showWelcomeStyle, onSubmit, onStop, references, status, iframeSkill.selectedSkills],
|
||||
);
|
||||
|
||||
const requestFormSubmit = useCallback(() => {
|
||||
|
|
@ -678,7 +679,7 @@ export function InputBox({
|
|||
<button
|
||||
ref={mentionTriggerRef}
|
||||
type="button"
|
||||
className="pointer-events-none absolute right-2 bottom-2 h-0 w-0 opacity-0"
|
||||
className="pointer-events-none absolute right-2 bottom-0 h-0 w-0 opacity-0"
|
||||
aria-hidden="true"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
|
|
@ -687,7 +688,7 @@ export function InputBox({
|
|||
align="start"
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
className="w-[min(32rem,var(--radix-dropdown-menu-trigger-width)+28rem)] max-h-[578px] overflow-y-visible p-[20px]"
|
||||
className="w-[min(32rem,var(--radix-dropdown-menu-trigger-width)+28rem)] max-h-[400px] overflow-y-visible p-[20px]"
|
||||
data-testid="mention-candidate-panel"
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -936,6 +937,7 @@ function SuggestionList({
|
|||
}) {
|
||||
const { t } = useI18n();
|
||||
const { textInput } = usePromptInputController();
|
||||
const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null);
|
||||
const suggestions = t.inputBox.suggestions;
|
||||
const promptSuggestions = suggestions.filter(
|
||||
(
|
||||
|
|
@ -1001,18 +1003,114 @@ function SuggestionList({
|
|||
},
|
||||
[bootstrapAndLockSkills, isBootstrapping, textInput],
|
||||
);
|
||||
|
||||
const handleSuggestionChildClick = useCallback(
|
||||
({
|
||||
childSkill,
|
||||
suggestionTitle,
|
||||
}: {
|
||||
childSkill: SelectedSkillPayloadItem;
|
||||
suggestionTitle: string;
|
||||
}) => {
|
||||
if (isBootstrapping) return;
|
||||
setOpenDropdownKey(null);
|
||||
const id = String(childSkill.id).trim();
|
||||
const name = childSkill.name?.trim() ?? "";
|
||||
if (!id || !name) return;
|
||||
void bootstrapAndLockSkills({
|
||||
selectedSkills: [{ id, name, detail: childSkill.detail }],
|
||||
title: suggestionTitle,
|
||||
});
|
||||
},
|
||||
[bootstrapAndLockSkills, isBootstrapping],
|
||||
);
|
||||
|
||||
return (
|
||||
<Suggestions
|
||||
className="min-h-16 w-fit items-start"
|
||||
data-testid="welcome-suggestions"
|
||||
>
|
||||
{promptSuggestions.map((suggestion) => (
|
||||
<Suggestion
|
||||
key={suggestion.suggestion}
|
||||
icon={suggestion.icon}
|
||||
suggestion={suggestion.suggestion}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
/>
|
||||
(() => {
|
||||
const childSkills = (suggestion.children ?? [])
|
||||
.map((item) => ({
|
||||
id: String(item.id).trim(),
|
||||
name: item.name?.trim() ?? "",
|
||||
detail: item.detail?.trim() ?? "",
|
||||
}))
|
||||
.filter(
|
||||
(
|
||||
item,
|
||||
): item is {
|
||||
id: string;
|
||||
name: string;
|
||||
detail: string;
|
||||
} => Boolean(item.id) && Boolean(item.name),
|
||||
);
|
||||
|
||||
if (childSkills.length === 0) {
|
||||
return (
|
||||
<Suggestion
|
||||
key={suggestion.suggestion}
|
||||
icon={suggestion.icon}
|
||||
suggestion={suggestion.suggestion}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const dropdownKey = suggestion.suggestion;
|
||||
return (
|
||||
<DropdownMenu
|
||||
key={dropdownKey}
|
||||
modal={false}
|
||||
open={openDropdownKey === dropdownKey}
|
||||
onOpenChange={(open) => setOpenDropdownKey(open ? dropdownKey : null)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Suggestion
|
||||
icon={suggestion.icon}
|
||||
suggestion={suggestion.suggestion}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
onMouseEnter={() => setOpenDropdownKey(dropdownKey)}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="min-w-[240px] rounded-[20px] p-[20px]"
|
||||
onMouseEnter={() => setOpenDropdownKey(dropdownKey)}
|
||||
onMouseLeave={() => setOpenDropdownKey(null)}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 text-[14px] text-[#333333]">
|
||||
{suggestion.suggestion}
|
||||
</DropdownMenuLabel >
|
||||
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
|
||||
<DropdownMenuGroup className=" pt-[20px] px-0">
|
||||
{childSkills.map((item) => (
|
||||
<Tooltip
|
||||
key={`${dropdownKey}-${item.id}`}
|
||||
content={item.detail || item.name}
|
||||
side="right"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer rounded-[10px] px-3 py-2"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
handleSuggestionChildClick({
|
||||
childSkill: item,
|
||||
suggestionTitle: suggestion.suggestion,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</DropdownMenuItem>
|
||||
</Tooltip>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
})()
|
||||
))}
|
||||
</Suggestions>
|
||||
);
|
||||
|
|
@ -1148,11 +1246,11 @@ function IframeSkillDialogButton({
|
|||
key={`${skill.skill_id}-${skill.title}-${index}`}
|
||||
className="shrink-0"
|
||||
>
|
||||
{skill.title}
|
||||
<span className="text-[12px]/[12px]">{skill.title}</span>
|
||||
{/* TODO: 因为后端接口不支持取消选择skill,所以暂时禁用取消选择按钮 */}
|
||||
<button
|
||||
onClick={() => clearSkill(skill.skill_id)}
|
||||
className="hover:bg-muted-foreground/20 ml-1 rounded-full"
|
||||
className="hover:bg-muted-foreground/20 ml-1 inline-flex size-4 items-center justify-center rounded-full align-middle"
|
||||
type="button"
|
||||
>
|
||||
<XIcon className="size-3" />
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
extractContentFromMessage,
|
||||
extractReasoningContentFromMessage,
|
||||
parseUploadedFiles,
|
||||
stripPriorityHintSuffix,
|
||||
stripUploadedFilesTag,
|
||||
type FileInMessage,
|
||||
} from "@/core/messages/utils";
|
||||
|
|
@ -166,7 +167,9 @@ function MessageContent_({
|
|||
|
||||
const contentToDisplay = useMemo(() => {
|
||||
if (isHuman) {
|
||||
return rawContent ? stripUploadedFilesTag(rawContent) : "";
|
||||
return rawContent
|
||||
? stripPriorityHintSuffix(stripUploadedFilesTag(rawContent))
|
||||
: "";
|
||||
}
|
||||
return rawContent ?? "";
|
||||
}, [rawContent, isHuman]);
|
||||
|
|
@ -421,22 +424,22 @@ function RichFileCard({
|
|||
/>
|
||||
</a>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="min-w-[120px] p-1">
|
||||
<ContextMenuItem
|
||||
disabled={!canReference}
|
||||
onSelect={() => {
|
||||
if (!file.path) return;
|
||||
dispatchMentionReference({
|
||||
threadId,
|
||||
filename: file.filename,
|
||||
path: file.path,
|
||||
ref_source: refSource,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t.common.reference}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
<ContextMenuContent className="min-w-[120px]">
|
||||
<ContextMenuItem
|
||||
disabled={!canReference}
|
||||
onClick={() => {
|
||||
if (!file.path) return;
|
||||
dispatchMentionReference({
|
||||
threadId,
|
||||
filename: file.filename,
|
||||
path: file.path,
|
||||
ref_source: refSource,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t.common.reference}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { Children, type ComponentProps, isValidElement } from "react";
|
||||
|
||||
import {
|
||||
Tooltip as TooltipPrimitive,
|
||||
TooltipContent,
|
||||
|
|
@ -9,15 +11,23 @@ import {
|
|||
export function Tooltip({
|
||||
children,
|
||||
content,
|
||||
side,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
side?: ComponentProps<typeof TooltipContent>["side"];
|
||||
content?: React.ReactNode;
|
||||
}) {
|
||||
const hasSingleElementChild =
|
||||
Children.count(children) === 1 && isValidElement(children);
|
||||
const triggerChild = hasSingleElementChild ? children : <span>{children}</span>;
|
||||
|
||||
return (
|
||||
<TooltipPrimitive delayDuration={500} {...props}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent>{content}</TooltipContent>
|
||||
<TooltipTrigger asChild>{triggerChild}</TooltipTrigger>
|
||||
<TooltipContent side={side}>
|
||||
<span className="whitespace-pre-line">{content}</span>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { LucideIcon } from "lucide-react";
|
|||
export interface SelectedSkillPayloadItem {
|
||||
id: string | number;
|
||||
name: string;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface Translations {
|
||||
|
|
|
|||
|
|
@ -126,31 +126,66 @@ export const zhCN: Translations = {
|
|||
prompt:
|
||||
"为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。",
|
||||
icon: PenLineIcon,
|
||||
children: [{ id: "6057", name: "生辰解语" }],
|
||||
children: [
|
||||
{
|
||||
id: "6057",
|
||||
name: "生辰解语",
|
||||
detail:
|
||||
"四柱八字命理分析。\n当用户询问八字、四柱、命理、算命、Bazi、运势预测、命盘分析,\n或想了解其八字命盘、运势、大运、流年时,请使用此功能。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
suggestion: "小红书种草",
|
||||
prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。",
|
||||
icon: CompassIcon,
|
||||
children: [{ id: "6099", name: "小红书笔记智造官" }],
|
||||
children: [
|
||||
{
|
||||
id: "6099",
|
||||
name: "小红书笔记智造官",
|
||||
detail:
|
||||
"根据用户需求及提供资料,撰写小红书笔记内容(标题与正文)。\n生成图片卡片(封面及正文卡片),并支持发布小红书笔记。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
suggestion: "精美报告",
|
||||
prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。",
|
||||
icon: GraduationCapIcon,
|
||||
children: [{ id: "6100", name: "MD 转 PDF 助手" }],
|
||||
children: [
|
||||
{
|
||||
id: "6100",
|
||||
name: "MD 转 PDF 助手",
|
||||
detail:
|
||||
"将 Markdown(.md)文件转换为专业排版的 PDF 文档。\n支持将 markdown 转换为 PDF。\n支持将 .md 文件转为 .pdf。\n支持从 markdown 生成 PDF 报告。\n适用于中文技术报告、学术论文、文档编写及各类专业排版需求。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
suggestion: "excel数据处理",
|
||||
prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。",
|
||||
icon: MicroscopeIcon,
|
||||
children: [{ id: "17", name: "Excel处理" }],
|
||||
children: [
|
||||
{
|
||||
id: "17",
|
||||
name: "Excel处理",
|
||||
detail:
|
||||
"全面的电子表格创建、编辑与分析功能。\n支持公式运算、格式设置、数据分析和可视化呈现。\n当 Claude 需要处理各类电子表格文件(如 .xlsx、.xlsm、.csv、.tsv 等格式)时,可执行以下操作:\n(1) 创建包含公式与格式的新表格;\n(2) 读取或分析表格数据;\n(3) 在保留公式的前提下修改现有表格;\n(4) 在表格内进行数据分析与可视化处理;\n(5) 重新计算公式。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
suggestion: "营销策划",
|
||||
prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。",
|
||||
icon: ShapesIcon,
|
||||
children: [{ id: "217", name: "产品营销背景" }],
|
||||
children: [
|
||||
{
|
||||
id: "217",
|
||||
name: "产品营销背景",
|
||||
detail:
|
||||
"当用户需要创建或更新产品营销背景文档时使用。\n可用于沉淀目标用户、市场环境与竞争格局等关键信息。",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
suggestionsCreate: [
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export interface SelectedSkillMessage {
|
|||
export interface SelectedSkillPayloadItem {
|
||||
id: string | number;
|
||||
name: string;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
|
@ -107,8 +108,16 @@ export function isSelectedSkillsMessage(
|
|||
if (!skill) return false;
|
||||
const id = skill.id;
|
||||
const name = skill.name;
|
||||
const detail = skill.detail;
|
||||
const isValidId = typeof id === "string" || typeof id === "number";
|
||||
return isValidId && typeof name === "string" && name.trim().length > 0;
|
||||
const isValidDetail =
|
||||
detail === undefined || typeof detail === "string";
|
||||
return (
|
||||
isValidId &&
|
||||
typeof name === "string" &&
|
||||
name.trim().length > 0 &&
|
||||
isValidDetail
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -351,6 +351,19 @@ export function stripUploadedFilesTag(content: string): string {
|
|||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the appended priority-hint suffix from a message content.
|
||||
* Suffix format:
|
||||
* - XClaw优先使用【附件...】
|
||||
* - XClaw优先使用【Skill...】
|
||||
* - XClaw优先使用【附件...】和【Skill...】
|
||||
*/
|
||||
export function stripPriorityHintSuffix(content: string): string {
|
||||
return content
|
||||
.replace(/\n?XClaw优先使用【[^】]+】(?:和【[^】]+】)?\s*$/u, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function parseUploadedFiles(content: string): FileInMessage[] {
|
||||
// Match <uploaded_files>...</uploaded_files> tag
|
||||
const uploadedFilesRegex = /<uploaded_files>([\s\S]*?)<\/uploaded_files>/;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import test from "node:test";
|
|||
const { buildFilesForSubmit } = await import(
|
||||
new URL("./submit-files.ts", import.meta.url).href
|
||||
);
|
||||
const { buildPriorityHintText, composeSubmitText } = await import(
|
||||
new URL("./priority-hint.ts", import.meta.url).href
|
||||
);
|
||||
|
||||
void test("buildFilesForSubmit keeps uploads and appends valid references", () => {
|
||||
const result = buildFilesForSubmit(
|
||||
|
|
@ -67,3 +70,56 @@ void test("buildFilesForSubmit keeps artifact mention path without re-upload", (
|
|||
assert.equal(result.files[0]?.path, "/mnt/user-data/artifacts/image.png");
|
||||
assert.equal(result.files[0]?.ref_source, "artifact");
|
||||
});
|
||||
|
||||
void test("buildPriorityHintText keeps attachments first and skills second", () => {
|
||||
const result = buildPriorityHintText({
|
||||
attachmentNames: ["spec.md"],
|
||||
skillIds: ["skill.docs.generate"],
|
||||
});
|
||||
assert.equal(result, "XClaw优先使用【spec.md】和【skill.docs.generate】");
|
||||
});
|
||||
|
||||
void test("buildPriorityHintText outputs single category when the other is empty", () => {
|
||||
assert.equal(
|
||||
buildPriorityHintText({
|
||||
attachmentNames: ["spec.md"],
|
||||
skillIds: [],
|
||||
}),
|
||||
"XClaw优先使用【spec.md】",
|
||||
);
|
||||
assert.equal(
|
||||
buildPriorityHintText({
|
||||
attachmentNames: [],
|
||||
skillIds: ["skill.docs.generate"],
|
||||
}),
|
||||
"XClaw优先使用【skill.docs.generate】",
|
||||
);
|
||||
});
|
||||
|
||||
void test("buildPriorityHintText deduplicates case-insensitively", () => {
|
||||
const result = buildPriorityHintText({
|
||||
attachmentNames: ["Spec.md", "spec.md", " SPEC.md "],
|
||||
skillIds: ["skill.excel", "SKILL.EXCEL"],
|
||||
});
|
||||
assert.equal(result, "XClaw优先使用【Spec.md】和【skill.excel】");
|
||||
});
|
||||
|
||||
void test("composeSubmitText appends hint only when needed", () => {
|
||||
assert.equal(
|
||||
composeSubmitText({
|
||||
baseText: "请总结",
|
||||
attachmentNames: ["spec.md"],
|
||||
skillIds: [],
|
||||
}),
|
||||
"请总结\nXClaw优先使用【spec.md】",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
composeSubmitText({
|
||||
baseText: "请总结",
|
||||
attachmentNames: [],
|
||||
skillIds: [],
|
||||
}),
|
||||
"请总结",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import type {
|
||||
PromptInputMessage,
|
||||
} from "@/components/ai-elements/prompt-input";
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
|
||||
import { getAPIClient } from "../api";
|
||||
import { getBackendBaseURL } from "../config";
|
||||
|
|
@ -20,6 +18,7 @@ import { listUploadedFiles, uploadFiles } from "../uploads";
|
|||
import type { UploadTarget } from "../uploads/api";
|
||||
|
||||
import { buildFilesForSubmit } from "./submit-files";
|
||||
import { buildPriorityHintText, composeSubmitText } from "./priority-hint";
|
||||
import type {
|
||||
AgentThread,
|
||||
AgentThreadContext,
|
||||
|
|
@ -148,6 +147,8 @@ function normalizeThreadId(
|
|||
return normalized;
|
||||
}
|
||||
|
||||
export { buildPriorityHintText, composeSubmitText };
|
||||
|
||||
export function useThreadStreamLegacy({
|
||||
threadId,
|
||||
isNewThread,
|
||||
|
|
@ -256,27 +257,30 @@ export function useThreadStream({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const showStreamErrorToast = useCallback((error: unknown) => {
|
||||
const message = getStreamErrorMessage(error);
|
||||
if (isStreamCancellation(error, message)) {
|
||||
// Cancellation is expected when user presses "Stop" or stream disconnects.
|
||||
console.info("[useThreadStream] stream cancelled:", message);
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const lastToast = lastErrorToastRef.current;
|
||||
if (
|
||||
lastToast &&
|
||||
lastToast.message === message &&
|
||||
now - lastToast.timestamp < STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lastErrorToastRef.current = { message, timestamp: now };
|
||||
console.error("[useThreadStream] conversation stream error:", error);
|
||||
console.error("[useThreadStream] parsed error message:", message);
|
||||
toast.error(t.threads.streamError);
|
||||
}, [t.threads.streamError]);
|
||||
const showStreamErrorToast = useCallback(
|
||||
(error: unknown) => {
|
||||
const message = getStreamErrorMessage(error);
|
||||
if (isStreamCancellation(error, message)) {
|
||||
// Cancellation is expected when user presses "Stop" or stream disconnects.
|
||||
console.info("[useThreadStream] stream cancelled:", message);
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
const lastToast = lastErrorToastRef.current;
|
||||
if (
|
||||
lastToast &&
|
||||
lastToast.message === message &&
|
||||
now - lastToast.timestamp < STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lastErrorToastRef.current = { message, timestamp: now };
|
||||
console.error("[useThreadStream] conversation stream error:", error);
|
||||
console.error("[useThreadStream] parsed error message:", message);
|
||||
toast.error(t.threads.streamError);
|
||||
},
|
||||
[t.threads.streamError],
|
||||
);
|
||||
|
||||
const handleStreamStart = useCallback(
|
||||
(_threadId: string) => {
|
||||
|
|
@ -401,6 +405,12 @@ export function useThreadStream({
|
|||
sendInFlightRef.current = true;
|
||||
|
||||
const text = message.text.trim();
|
||||
const referenceNames = (message.references ?? []).map(
|
||||
(reference) => reference.filename,
|
||||
);
|
||||
const selectedSkillIds = (message.selectedSkills ?? []).map(
|
||||
(skill) => skill.skill_id,
|
||||
);
|
||||
const resolvedThreadId =
|
||||
normalizeThreadId(threadId) ??
|
||||
normalizeThreadId(threadIdRef.current) ??
|
||||
|
|
@ -501,9 +511,7 @@ export function useThreadStream({
|
|||
const failedConversions = conversionResults.length - files.length;
|
||||
|
||||
if (failedConversions > 0) {
|
||||
throw new Error(
|
||||
t.threads.uploadPrepareFailed(failedConversions),
|
||||
);
|
||||
throw new Error(t.threads.uploadPrepareFailed(failedConversions));
|
||||
}
|
||||
|
||||
if (!resolvedThreadId) {
|
||||
|
|
@ -563,6 +571,16 @@ export function useThreadStream({
|
|||
toast.error(t.threads.staleReferencesRemoved);
|
||||
}
|
||||
|
||||
const uploadedNames =
|
||||
uploadedFileInfo.length > 0
|
||||
? uploadedFileInfo.map((file) => file.filename)
|
||||
: (message.files ?? []).map((file) => file.filename ?? "");
|
||||
const submitText = composeSubmitText({
|
||||
baseText: text,
|
||||
attachmentNames: [...uploadedNames, ...referenceNames],
|
||||
skillIds: selectedSkillIds,
|
||||
});
|
||||
|
||||
await thread.submit(
|
||||
{
|
||||
messages: [
|
||||
|
|
@ -571,7 +589,7 @@ export function useThreadStream({
|
|||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text,
|
||||
text: submitText,
|
||||
},
|
||||
],
|
||||
additional_kwargs:
|
||||
|
|
@ -671,6 +689,12 @@ export function useSubmitThread({
|
|||
return;
|
||||
}
|
||||
const text = message.text.trim();
|
||||
const referenceNames = (message.references ?? []).map(
|
||||
(reference) => reference.filename,
|
||||
);
|
||||
const selectedSkillIds = (message.selectedSkills ?? []).map(
|
||||
(skill) => skill.skill_id,
|
||||
);
|
||||
|
||||
const hasFiles = !!(message.files && message.files.length > 0);
|
||||
const hasReferences = !!(
|
||||
|
|
@ -736,6 +760,15 @@ export function useSubmitThread({
|
|||
toast.error(t.threads.staleReferencesRemoved);
|
||||
}
|
||||
|
||||
const uploadedNames = (message.files ?? []).map(
|
||||
(file) => file.filename ?? "",
|
||||
);
|
||||
const submitText = composeSubmitText({
|
||||
baseText: text,
|
||||
attachmentNames: [...uploadedNames, ...referenceNames],
|
||||
skillIds: selectedSkillIds,
|
||||
});
|
||||
|
||||
await thread.submit(
|
||||
{
|
||||
messages: [
|
||||
|
|
@ -744,7 +777,7 @@ export function useSubmitThread({
|
|||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text,
|
||||
text: submitText,
|
||||
},
|
||||
],
|
||||
additional_kwargs:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
function uniqueNormalizedValues(values: Array<string | undefined>): string[] {
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const value of values) {
|
||||
const normalized = value?.trim();
|
||||
if (!normalized) continue;
|
||||
const dedupeKey = normalized.toLocaleLowerCase();
|
||||
if (seen.has(dedupeKey)) continue;
|
||||
seen.add(dedupeKey);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function buildPriorityHintText({
|
||||
attachmentNames,
|
||||
skillIds,
|
||||
}: {
|
||||
attachmentNames: string[];
|
||||
skillIds: string[];
|
||||
}): string {
|
||||
const attachments = uniqueNormalizedValues(attachmentNames);
|
||||
const skills = uniqueNormalizedValues(skillIds);
|
||||
if (attachments.length === 0 && skills.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const attachmentPart =
|
||||
attachments.length > 0 ? `【${attachments.join("、")}】` : "";
|
||||
const skillPart = skills.length > 0 ? `【${skills.join("、")}】` : "";
|
||||
|
||||
if (attachmentPart && skillPart) {
|
||||
return `XClaw优先使用${attachmentPart}和${skillPart}`;
|
||||
}
|
||||
return `XClaw优先使用${attachmentPart || skillPart}`;
|
||||
}
|
||||
|
||||
export function composeSubmitText({
|
||||
baseText,
|
||||
attachmentNames,
|
||||
skillIds,
|
||||
}: {
|
||||
baseText: string;
|
||||
attachmentNames: string[];
|
||||
skillIds: string[];
|
||||
}): string {
|
||||
const trimmedBase = baseText.trim();
|
||||
if (!trimmedBase) return trimmedBase;
|
||||
const priorityHint = buildPriorityHintText({
|
||||
attachmentNames,
|
||||
skillIds,
|
||||
});
|
||||
if (!priorityHint) return trimmedBase;
|
||||
return `${trimmedBase}\n${priorityHint}`;
|
||||
}
|
||||
|
|
@ -30,6 +30,26 @@ function getThreadStorageKey(threadId?: string | null): string | null {
|
|||
return `${STORAGE_KEYS.byThreadPrefix}${normalized}`;
|
||||
}
|
||||
|
||||
function persistSkillsToSessionStorage(
|
||||
skills: SkillData[],
|
||||
threadId?: string | null,
|
||||
) {
|
||||
const threadKey = getThreadStorageKey(threadId);
|
||||
if (skills.length === 0) {
|
||||
window.sessionStorage.removeItem(STORAGE_KEYS.latest);
|
||||
if (threadKey) {
|
||||
window.sessionStorage.removeItem(threadKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(skills);
|
||||
window.sessionStorage.setItem(STORAGE_KEYS.latest, payload);
|
||||
if (threadKey) {
|
||||
window.sessionStorage.setItem(threadKey, payload);
|
||||
}
|
||||
}
|
||||
|
||||
function parseStoredSkills(raw: string | null): SkillData[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
|
|
@ -103,39 +123,39 @@ export function useIframeSkill(
|
|||
return next;
|
||||
});
|
||||
|
||||
// 2) 回滚 localStorage(latest + thread)
|
||||
// 2) 回滚 sessionStorage(latest + thread)
|
||||
const latestSkills = parseStoredSkills(
|
||||
window.localStorage.getItem(STORAGE_KEYS.latest),
|
||||
window.sessionStorage.getItem(STORAGE_KEYS.latest),
|
||||
);
|
||||
const nextLatestSkills = removeSkillsByIdsFromList(
|
||||
latestSkills,
|
||||
skillIds,
|
||||
);
|
||||
if (nextLatestSkills.length > 0) {
|
||||
window.localStorage.setItem(
|
||||
window.sessionStorage.setItem(
|
||||
STORAGE_KEYS.latest,
|
||||
JSON.stringify(nextLatestSkills),
|
||||
);
|
||||
} else {
|
||||
window.localStorage.removeItem(STORAGE_KEYS.latest);
|
||||
window.sessionStorage.removeItem(STORAGE_KEYS.latest);
|
||||
}
|
||||
|
||||
const threadKey = getThreadStorageKey(threadId);
|
||||
if (threadKey) {
|
||||
const threadSkills = parseStoredSkills(
|
||||
window.localStorage.getItem(threadKey),
|
||||
window.sessionStorage.getItem(threadKey),
|
||||
);
|
||||
const nextThreadSkills = removeSkillsByIdsFromList(
|
||||
threadSkills,
|
||||
skillIds,
|
||||
);
|
||||
if (nextThreadSkills.length > 0) {
|
||||
window.localStorage.setItem(
|
||||
window.sessionStorage.setItem(
|
||||
threadKey,
|
||||
JSON.stringify(nextThreadSkills),
|
||||
);
|
||||
} else {
|
||||
window.localStorage.removeItem(threadKey);
|
||||
window.sessionStorage.removeItem(threadKey);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -196,39 +216,27 @@ export function useIframeSkill(
|
|||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, []);
|
||||
|
||||
// 3. 首次进入时恢复 localStorage 中上次选择的 skill(线程优先,其次全局)
|
||||
useEffect(() => {
|
||||
const threadKey = getThreadStorageKey(threadId);
|
||||
const threadSkills = threadKey
|
||||
? parseStoredSkills(window.localStorage.getItem(threadKey))
|
||||
: [];
|
||||
const latestSkills = parseStoredSkills(
|
||||
window.localStorage.getItem(STORAGE_KEYS.latest),
|
||||
);
|
||||
const restoredSkills =
|
||||
threadSkills.length > 0 ? threadSkills : latestSkills;
|
||||
if (restoredSkills.length === 0) return;
|
||||
setSelectedSkills(restoredSkills);
|
||||
setSelectedSkill(restoredSkills[0] ?? null);
|
||||
}, [threadId]);
|
||||
// 3. 首次进入时恢复 sessionStorage 中上次选择的 skill(线程优先,其次全局)
|
||||
// 已按需求注释:关闭页签后重新打开时,不再从 sessionStorage 自动恢复。
|
||||
// useEffect(() => {
|
||||
// const threadKey = getThreadStorageKey(threadId);
|
||||
// const threadSkills = threadKey
|
||||
// ? parseStoredSkills(window.sessionStorage.getItem(threadKey))
|
||||
// : [];
|
||||
// const latestSkills = parseStoredSkills(
|
||||
// window.sessionStorage.getItem(STORAGE_KEYS.latest),
|
||||
// );
|
||||
// const restoredSkills =
|
||||
// threadSkills.length > 0 ? threadSkills : latestSkills;
|
||||
// if (restoredSkills.length === 0) return;
|
||||
// setSelectedSkills(restoredSkills);
|
||||
// setSelectedSkill(restoredSkills[0] ?? null);
|
||||
// }, [threadId]);
|
||||
|
||||
// 4. 选择变化时同步到 localStorage
|
||||
// 4. 选择变化时同步到 sessionStorage
|
||||
useEffect(() => {
|
||||
const threadKey = getThreadStorageKey(threadId);
|
||||
if (selectedSkills.length === 0) {
|
||||
// 空数组也要同步到存储,避免 UI 状态与缓存不一致
|
||||
window.localStorage.removeItem(STORAGE_KEYS.latest);
|
||||
if (threadKey) {
|
||||
window.localStorage.removeItem(threadKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(selectedSkills);
|
||||
window.localStorage.setItem(STORAGE_KEYS.latest, payload);
|
||||
if (threadKey) {
|
||||
window.localStorage.setItem(threadKey, payload);
|
||||
}
|
||||
// 空数组也要同步到存储,避免 UI 状态与缓存不一致
|
||||
persistSkillsToSessionStorage(selectedSkills, threadId);
|
||||
}, [selectedSkills, threadId]);
|
||||
|
||||
// 发送选择预定义 skill
|
||||
|
|
@ -285,6 +293,15 @@ export function useIframeSkill(
|
|||
});
|
||||
|
||||
try {
|
||||
// 发起 bootstrap 时立即同步缓存;失败会在 removeFailedSkills 中回滚。
|
||||
const normalizedSkills = selectedSkills.map((item) => ({
|
||||
skill_id: String(item.id),
|
||||
title: item.name,
|
||||
}));
|
||||
setSelectedSkill(normalizedSkills[0] ?? null);
|
||||
setSelectedSkills(normalizedSkills);
|
||||
persistSkillsToSessionStorage(normalizedSkills, threadId);
|
||||
|
||||
const result = await bootstrapRemoteSkill({
|
||||
thread_id: threadId,
|
||||
content_ids,
|
||||
|
|
@ -307,12 +324,12 @@ export function useIframeSkill(
|
|||
}
|
||||
|
||||
sendSelectSkill(selectedSkills);
|
||||
const normalizedSkills = selectedSkills.map((item) => ({
|
||||
const latestSkills = selectedSkills.map((item) => ({
|
||||
skill_id: String(item.id),
|
||||
title: item.name,
|
||||
}));
|
||||
setSelectedSkill(normalizedSkills[0] ?? null);
|
||||
setSelectedSkills(normalizedSkills);
|
||||
setSelectedSkill(latestSkills[0] ?? null);
|
||||
setSelectedSkills(latestSkills);
|
||||
|
||||
toast.success(t.skills.loadSuccessWithTitle(title), {
|
||||
description: result.message || t.skills.createdFiles(result.created_files),
|
||||
|
|
@ -359,30 +376,30 @@ export function useIframeSkill(
|
|||
|
||||
// 同步 latest 缓存:仅删除对应 skill(或全部清空)
|
||||
const latestSkills = parseStoredSkills(
|
||||
window.localStorage.getItem(STORAGE_KEYS.latest),
|
||||
window.sessionStorage.getItem(STORAGE_KEYS.latest),
|
||||
);
|
||||
const nextLatestSkills = removeAll
|
||||
? []
|
||||
: latestSkills.filter((skill) => skill.skill_id !== String(skillId));
|
||||
if (nextLatestSkills.length > 0) {
|
||||
window.localStorage.setItem(
|
||||
window.sessionStorage.setItem(
|
||||
STORAGE_KEYS.latest,
|
||||
JSON.stringify(nextLatestSkills),
|
||||
);
|
||||
} else {
|
||||
window.localStorage.removeItem(STORAGE_KEYS.latest);
|
||||
window.sessionStorage.removeItem(STORAGE_KEYS.latest);
|
||||
}
|
||||
|
||||
// 同步线程缓存:保存剩余数组,空则删除 key
|
||||
const threadKey = getThreadStorageKey(threadId);
|
||||
if (threadKey) {
|
||||
if (nextSelectedSkills.length > 0) {
|
||||
window.localStorage.setItem(
|
||||
window.sessionStorage.setItem(
|
||||
threadKey,
|
||||
JSON.stringify(nextSelectedSkills),
|
||||
);
|
||||
} else {
|
||||
window.localStorage.removeItem(threadKey);
|
||||
window.sessionStorage.removeItem(threadKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -462,12 +462,12 @@ pre{
|
|||
|
||||
/* 二三级标题 - 16px */
|
||||
[data-streamdown="heading-2"],
|
||||
[data-streamdown="heading-3"] {
|
||||
[data-streamdown="heading-3"],[data-streamdown="heading-4"] {
|
||||
font-size: calc(16px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
/* 代码块 - 14px */
|
||||
[data-streamdown="code-block"] pre {
|
||||
[data-streamdown="code-block"] pre,code {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
|
|
@ -481,13 +481,19 @@ pre{
|
|||
|
||||
[data-streamdown="table-cell"] {
|
||||
background-color: transparent;
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
height:calc(42px * var(--zoom-scale)) ;
|
||||
}
|
||||
[data-streamdown="table-header"] {
|
||||
background: #9c9b9b26;
|
||||
height: 50px;
|
||||
height: calc(50px * var(--zoom-scale));
|
||||
}
|
||||
[data-streamdown="table-header"] th {
|
||||
text-align: center;
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
[data-slot="hover-card-trigger"] [data-slot="badge"]{
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -500,7 +506,7 @@ pre{
|
|||
border-top-right-radius: 5px;
|
||||
}
|
||||
[data-streamdown="table-body"] tr:first-child td{
|
||||
padding-top: 20px;
|
||||
padding-top: calc(20px * var(--zoom-scale));
|
||||
}
|
||||
/* 行分隔线 */
|
||||
[data-streamdown="table-body"] tr{
|
||||
|
|
@ -515,14 +521,13 @@ pre{
|
|||
}
|
||||
|
||||
[data-streamdown="table-body"] tr:last-child {
|
||||
height: 50px;
|
||||
padding-top: calc(50px * var(--zoom-scale));
|
||||
|
||||
}
|
||||
[data-streamdown="table-row"] >[data-streamdown="table-cell"]{
|
||||
line-height: 14px;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -141,10 +141,9 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
|||
).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("DF-INPUT-007 输入@时展示文件候选并可选择为引用 chip", async (
|
||||
{ page },
|
||||
testInfo,
|
||||
) => {
|
||||
test("DF-INPUT-007 输入@时展示文件候选并可选择为引用 chip", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_WITH_REFERENCE_FIXTURE,
|
||||
|
|
@ -174,10 +173,9 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
|||
await expect(panel).toBeHidden();
|
||||
});
|
||||
|
||||
test("DF-INPUT-008 失效引用不会阻断文本发送(可解释 skip)", async (
|
||||
{ page },
|
||||
testInfo,
|
||||
) => {
|
||||
test("DF-INPUT-008 失效引用不会阻断文本发送(可解释 skip)", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_WITH_STALE_REFERENCE,
|
||||
|
|
@ -230,7 +228,9 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
|||
request.method() === "POST" &&
|
||||
request
|
||||
.url()
|
||||
.includes(`/api/langgraph/threads/${THREAD_WITH_STALE_REFERENCE}/runs/stream`),
|
||||
.includes(
|
||||
`/api/langgraph/threads/${THREAD_WITH_STALE_REFERENCE}/runs/stream`,
|
||||
),
|
||||
);
|
||||
await page.locator("button[aria-label='Submit']").click();
|
||||
await expect.poll(() => staleArtifactRequested).toBe(true);
|
||||
|
|
@ -242,10 +242,59 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
|||
await expect(page.locator("textarea[name='message']")).toHaveValue("");
|
||||
});
|
||||
|
||||
test("DF-INPUT-009 引用上限为 10,第 11 个被阻止并提示", async (
|
||||
{ page },
|
||||
testInfo,
|
||||
) => {
|
||||
test("DF-INPUT-008A 提交态附加优先提示但消息区只显示原文", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_WITH_REFERENCE_FIXTURE,
|
||||
"FRONTEND_E2E_ARTIFACTS_THREAD_ID 或 FRONTEND_E2E_THREAD_ID",
|
||||
);
|
||||
await stubReferenceFixtures(page, {
|
||||
threadId: THREAD_WITH_REFERENCE_FIXTURE!,
|
||||
artifactPaths: [REFERENCE_ARTIFACT_PATH],
|
||||
uploadFiles: REFERENCE_UPLOAD_FIXTURES,
|
||||
});
|
||||
await openChat(page, reuseThreadChatEntry(THREAD_WITH_REFERENCE_FIXTURE!));
|
||||
|
||||
await expandComposer(page);
|
||||
const textarea = page.locator("textarea[name='message']");
|
||||
const userInput = "请根据引用文件给出摘要";
|
||||
await textarea.fill(`${userInput} `);
|
||||
|
||||
await openReferencePicker(page);
|
||||
const firstReference = page.getByTestId("mention-candidate-item").first();
|
||||
await expect(firstReference).toBeVisible();
|
||||
const referenceName =
|
||||
REFERENCE_UPLOAD_FIXTURES[0]?.filename ?? "fixture-01.md";
|
||||
await firstReference.click();
|
||||
await expect(page.getByTestId("reference-chip")).toHaveCount(1);
|
||||
|
||||
const submitRequest = page.waitForRequest(
|
||||
(request) =>
|
||||
request.method() === "POST" &&
|
||||
request
|
||||
.url()
|
||||
.includes(
|
||||
`/api/langgraph/threads/${THREAD_WITH_REFERENCE_FIXTURE}/runs/stream`,
|
||||
),
|
||||
);
|
||||
|
||||
await page.locator("button[aria-label='Submit']").click();
|
||||
const request = await submitRequest;
|
||||
const requestBody = request.postData() ?? "";
|
||||
|
||||
expect(requestBody).toContain(userInput);
|
||||
expect(requestBody).toContain("XClaw优先使用【");
|
||||
expect(requestBody).toContain(referenceName);
|
||||
await expect(
|
||||
page.locator(".is-user").filter({ hasText: "XClaw优先使用【" }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("DF-INPUT-009 引用上限为 10,第 11 个被阻止并提示", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_WITH_REFERENCE_FIXTURE,
|
||||
|
|
@ -291,8 +340,6 @@ test.describe("聊天工作台 / 输入区与发送", () => {
|
|||
});
|
||||
|
||||
await expect(page.getByTestId("reference-chip-remove")).toHaveCount(10);
|
||||
await expect(
|
||||
toastByText(page, "单条消息最多引用 10 个文件"),
|
||||
).toBeVisible();
|
||||
await expect(toastByText(page, "单条消息最多引用 10 个文件")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue