Refactor AI document workflow
This commit is contained in:
1 parent
d524c94074
commit
c2ef5e32dd
59 files changed
+1656
-1739
No files matched your search
@@ -4,12 +4,14 @@
|
||||
### 1.1 AI相关
|
||||
- 当涉及 AI 架构、主流程或 human-only runtime 时,必须阅读 `doc/ai-design/architecture.md`
|
||||
- 当涉及 AI 实现、运行入口、跨层边界或工作区 KB 范围时,必须阅读 `doc/ai-design/implementation-spec.md` 与 `doc/ai-design/data-definition-and-flow.md`
|
||||
- 当涉及 `mouth` 的 `reference_scope / runtime_artifacts` 组织方式,或 `FileReview` 运行态收口时,必须阅读 `doc/ai-design/implementation-spec.md` 与 `doc/ai-design/data-definition-and-flow.md`
|
||||
- 当涉及 human 的 skill 分类、builtin/external 边界或 skill 基线时,必须阅读 `doc/ai-design/human-skill-design.md`
|
||||
- 当涉及 prompt 结构、工具结果渲染、工具 schema/输出 shape 自动导出或 human AI-facing 文案时,必须阅读 `doc/ai-design/prompt-design.md`
|
||||
|
||||
### 1.2 docx相关
|
||||
- 当涉及 docx 编辑架构、source baseline、replay session 或三视图主线时,必须阅读 `doc/docx-design/architecture.md`
|
||||
- 当涉及 docx 实现约束、review/commit 链路、serializer 或样式继承时,必须阅读 `doc/docx-design/implementation-spec.md`
|
||||
- 当涉及单文件 `FileReview`、docx 审阅稿生成或提交尝试记录时,必须阅读 `doc/docx-design/implementation-spec.md` 与 `doc/database.md`
|
||||
- 当涉及 docx 数据结构、revision/replay node、artifact 关系或状态流转时,必须阅读 `doc/docx-design/data-definition-and-flow.md`
|
||||
|
||||
### 1.3 项目整体相关
|
||||
|
||||
@@ -142,7 +142,8 @@ interface AiRunContext {
|
||||
beginDocxReviewRun: () => void
|
||||
|
||||
// human task 完成后汇总本次 run 的 docx 改动并生成对应视图 artifact。
|
||||
finalizeDocxReviewRun: () => Promise<DraftReviewDetail[]>
|
||||
// 当前返回单文件 FileReviewDetail[];未来其他格式接入时应复用同一外壳。
|
||||
finalizeDocxReviewRun: () => Promise<FileReviewDetail[]>
|
||||
|
||||
// human task 中断或失败后丢弃本次 run 尚未收口的 docx 草稿审阅状态。
|
||||
discardDocxReviewRun: () => void
|
||||
|
||||
@@ -98,9 +98,9 @@ stateDiagram-v2
|
||||
### 5.4 `mouth`
|
||||
|
||||
- 职责:围绕 `task_goal` 把当前已经确定的状态组织成最终用户交付,并回写 `HumanResult`。
|
||||
- 主要读取:`task`、`memory.stable`、`memory.history.roundHistory`、`memory.history.loadedHistoryContext`、`execution.artifacts`。
|
||||
- 主要读取:`task`、`reference_scope`、`memory.stable`、`memory.history.roundHistory`、`memory.history.loadedHistoryContext`、`execution.runtimeArtifacts`。
|
||||
- 主要回写:`result`、`execution.status`、`execution.stage`、`trace.steps`。
|
||||
- 约束:模型输出必须通过 `ctx.chatText`;输出面向最终用户,优先直接完成用户原始诉求;优先依据 `memory.stable` 中的任务笔记组织交付,当笔记过于概括时再结合统一 `history` 与 `artifacts` 补足细节;可使用自由段落或列表组织,不要求固定报告模板;若开启 `emitAnswerEvents`,增量文本通过 `answer_token` 发出,完成后通过 `answer_completed` 收口。
|
||||
- 约束:模型输出必须通过 `ctx.chatText`;输出面向最终用户,但必须忠实于当前整体状态;`mouth` 优先吸收 `reference_scope`、`memory.stable`、统一 `history` 与 changed-files 视图的 `runtimeArtifacts`,不再直接消费 review 内部结构;可使用自由段落或列表组织,不要求固定报告模板;若开启 `emitAnswerEvents`,增量文本通过 `answer_token` 发出,完成后通过 `answer_completed` 收口。
|
||||
|
||||
## 6. 事件与交付
|
||||
|
||||
|
||||
+18
-34
@@ -21,7 +21,7 @@
|
||||
- `WebSourceStore`:`web_source`(网页资料来源 identity、抓取状态与来源级元数据)
|
||||
- `WorkspaceStore`:`workspace_source` / `workspace_node`(工作空间、目录树快照、准备状态与节点绑定)
|
||||
- `ChatStore`:`chat_session` / `chat_message`(会话与消息 CRUD)
|
||||
- `DraftReviewStore`:`draft_review` / `draft_review_document` / `draft_review_change` / `draft_commit_job`(草稿审阅、变更确认与提交结果)
|
||||
- `FileReviewStore`:`file_review` / `file_review_change` / `commit_attempt`(单文件审阅、变更确认与提交结果)
|
||||
- `LlmConfigStore`:`llm_config`(配置 CRUD 与激活态切换)
|
||||
- **组合根(Facade)**: `src/main/services/storage/StorageService.ts`
|
||||
- 统一持有 `core` 与各 domain store
|
||||
@@ -185,9 +185,9 @@
|
||||
| sources | Utf8(Nullable) | 引用来源(SearchResult[] 的 JSON 字符串) |
|
||||
| error | Boolean(Nullable) | 是否出错 |
|
||||
|
||||
### 表: draft_review(草稿审阅汇总表)
|
||||
### 表: file_review(单文件审阅表)
|
||||
|
||||
存储一次 AI 运行产生的待审阅结果集合。
|
||||
存储一次 AI 运行中某个单文件的待审阅结果。当前只正式用于 `docx`,未来 `xlsx / pptx` 可沿用同一外壳。
|
||||
|
||||
| 字段名 | 类型 | 描述 |
|
||||
| :-------------------- | :--------------- | :--- |
|
||||
@@ -196,7 +196,16 @@
|
||||
| run_id | Utf8 | 所属运行 |
|
||||
| status | Utf8 | 审阅状态 |
|
||||
| title | Utf8 | 审阅标题 |
|
||||
| document_count | Int32 | 文档数 |
|
||||
| source_document_id | Utf8(Nullable) | 源 document ID |
|
||||
| source_path | Utf8 | 源文件路径 |
|
||||
| source_name | Utf8 | 源文件名 |
|
||||
| format | Utf8 | 文件格式 |
|
||||
| review_render_kind | Utf8(Nullable) | 预览/审阅渲染类型 |
|
||||
| latest_draft_snapshot_id | Utf8 | 最新草稿快照 |
|
||||
| draft_artifact_path | Utf8(Nullable) | 待确认稿路径 |
|
||||
| working_artifact_path | Utf8(Nullable) | working artifact 路径 |
|
||||
| committed_artifact_path | Utf8(Nullable) | committed artifact 路径 |
|
||||
| source_version_json | Utf8 | 源版本信息 JSON |
|
||||
| change_count | Int32 | 变更总数 |
|
||||
| pending_change_count | Int32 | 待决策变更数 |
|
||||
| accepted_change_count | Int32 | 已接受变更数 |
|
||||
@@ -207,31 +216,7 @@
|
||||
| committed_at | Int64(Nullable)| 最终提交时间 |
|
||||
| last_error | Utf8(Nullable) | 最近错误 |
|
||||
|
||||
### 表: draft_review_document(草稿审阅文档表)
|
||||
|
||||
存储审阅中的单个草稿文档及其源文件版本信息。
|
||||
|
||||
| 字段名 | 类型 | 描述 |
|
||||
| :---------------------- | :--------------- | :--- |
|
||||
| id | Utf8 | 草稿文档 ID |
|
||||
| review_id | Utf8 | 所属审阅 ID |
|
||||
| source_document_id | Utf8(Nullable) | 源 document ID |
|
||||
| source_path | Utf8 | 源文件路径 |
|
||||
| source_name | Utf8 | 源文件名 |
|
||||
| format | Utf8 | 文档格式 |
|
||||
| latest_draft_snapshot_id| Utf8 | 最新草稿快照 |
|
||||
| review_artifact_path | Utf8(Nullable) | 审阅稿产物路径 |
|
||||
| working_artifact_path | Utf8(Nullable) | working draft 产物路径 |
|
||||
| committed_artifact_path | Utf8(Nullable) | committed view 产物路径 |
|
||||
| source_version_json | Utf8 | 源版本信息 JSON |
|
||||
| total_change_count | Int32 | 变更总数 |
|
||||
| pending_change_count | Int32 | 待决策变更数 |
|
||||
| accepted_change_count | Int32 | 已接受变更数 |
|
||||
| rejected_change_count | Int32 | 已拒绝变更数 |
|
||||
| created_at | Int64 | 创建时间 |
|
||||
| updated_at | Int64 | 更新时间 |
|
||||
|
||||
### 表: draft_review_change(草稿审阅变更表)
|
||||
### 表: file_review_change(单文件审阅变更表)
|
||||
|
||||
存储最小审阅单元及其接受/拒绝状态。
|
||||
|
||||
@@ -239,7 +224,6 @@
|
||||
| :------------------ | :--------------- | :--- |
|
||||
| id | Utf8 | 变更 ID |
|
||||
| review_id | Utf8 | 所属审阅 ID |
|
||||
| document_id | Utf8 | 所属草稿文档 ID |
|
||||
| order_index | Int32 | 排序索引 |
|
||||
| change_kind | Utf8 | 变更类型 |
|
||||
| title | Utf8 | 变更标题 |
|
||||
@@ -247,13 +231,14 @@
|
||||
| decision | Utf8 | `pending / accepted / rejected` |
|
||||
| before_text | Utf8 | 变更前文本 |
|
||||
| after_text | Utf8 | 变更后文本 |
|
||||
| locator_json | Utf8 | 定位信息 JSON |
|
||||
| review_artifact_path | Utf8(Nullable)| 局部审阅产物 |
|
||||
| locator_json | Utf8(Nullable) | 定位信息 JSON |
|
||||
| diff_hunks_json | Utf8(Nullable) | 统一 diff 片段 |
|
||||
| source_step_ref | Utf8(Nullable) | 来源步骤引用 |
|
||||
| metadata_json | Utf8(Nullable) | 附加元数据 |
|
||||
| created_at | Int64 | 创建时间 |
|
||||
| updated_at | Int64 | 更新时间 |
|
||||
|
||||
### 表: draft_commit_job(草稿提交任务表)
|
||||
### 表: commit_attempt(提交尝试表)
|
||||
|
||||
存储审阅通过后的最终提交执行结果。
|
||||
|
||||
@@ -262,7 +247,6 @@
|
||||
| id | Utf8 | 提交任务 ID |
|
||||
| review_id | Utf8 | 所属审阅 ID |
|
||||
| status | Utf8 | 提交状态 |
|
||||
| document_count| Int32 | 文档数量 |
|
||||
| results_json | Utf8 | 提交结果 JSON |
|
||||
| started_at | Int64 | 开始时间 |
|
||||
| completed_at | Int64(Nullable)| 完成时间 |
|
||||
|
||||
+87
-93
@@ -9,9 +9,9 @@ import { StorageService } from '../services/storage/StorageService'
|
||||
import { ModelService } from '../services/model/ModelService'
|
||||
import { AiRunService } from '../services/ai/AiRunService'
|
||||
import { updateSessionMemoryAfterRun } from '../services/ai/memory'
|
||||
import { DraftReviewService } from '../services/ai/human/tools/docx/review/DraftReviewService'
|
||||
import { FileReviewService } from '../services/ai/human/tools/docx/review/FileReviewService'
|
||||
import {
|
||||
getReviewArtifactPath,
|
||||
getDraftArtifactPath,
|
||||
getWorkingArtifactPath
|
||||
} from '../services/ai/human/tools/docx/review/viewProjection'
|
||||
import {
|
||||
@@ -44,25 +44,25 @@ import type {
|
||||
AiRunCancelResponse,
|
||||
AiRunApprovalDecisionRequest,
|
||||
AiRunApprovalDecisionResponse,
|
||||
CommitDraftReviewRequest,
|
||||
CommitDraftReviewResponse,
|
||||
CommitFileReviewRequest,
|
||||
CommitFileReviewResponse,
|
||||
CreateWorkspaceSourceRequest,
|
||||
DiscardDraftReviewRequest,
|
||||
DiscardDraftReviewResponse,
|
||||
DiscardFileReviewRequest,
|
||||
DiscardFileReviewResponse,
|
||||
DeleteWorkspaceSourceResponse,
|
||||
ListReferenceItemsRequest,
|
||||
ListDraftReviewsRequest,
|
||||
ListDraftReviewsResponse,
|
||||
ListFileReviewsRequest,
|
||||
ListFileReviewsResponse,
|
||||
ListWorkspaceNodesRequest,
|
||||
GetDraftReviewRequest,
|
||||
GetDraftReviewResponse,
|
||||
GetFileReviewRequest,
|
||||
GetFileReviewResponse,
|
||||
OpenPathWithApplicationRequest,
|
||||
OpenPathWithApplicationResponse,
|
||||
PickDirectoryResponse,
|
||||
PickApplicationResponse,
|
||||
ReferenceItemListResponse,
|
||||
ValidateDraftCommitRequest,
|
||||
ValidateDraftCommitResponse,
|
||||
ValidateFileReviewCommitRequest,
|
||||
ValidateFileReviewCommitResponse,
|
||||
ListWebSourcesRequest,
|
||||
RefreshWebSourceRequest,
|
||||
DeleteWebSourceRequest,
|
||||
@@ -265,7 +265,7 @@ export function registerHandlers(services: {
|
||||
const aiRuntimeSettingsStore = storageService.aiRuntimeSettings
|
||||
const webSourceStore = storageService.webSource
|
||||
const workspaceStore = storageService.workspace
|
||||
const draftReviewService = new DraftReviewService(storageService.draftReview, documentsStore)
|
||||
const fileReviewService = new FileReviewService(storageService.fileReview, documentsStore)
|
||||
const workspaceService = new WorkspaceSourceService()
|
||||
const webSourceService = new WebSourceService()
|
||||
|
||||
@@ -373,9 +373,9 @@ export function registerHandlers(services: {
|
||||
})
|
||||
}
|
||||
|
||||
const stageE2EDraftReviewDocument = async (input: {
|
||||
const stageE2EFileReview = async (input: {
|
||||
runId: string
|
||||
draftReviewService: DraftReviewService
|
||||
fileReviewService: FileReviewService
|
||||
sourceFileName: string
|
||||
acceptedReplace: string
|
||||
rejectedReplace: string
|
||||
@@ -397,7 +397,7 @@ export function registerHandlers(services: {
|
||||
throw new Error(openResponse.ok ? 'e2e review open worker mismatch' : openResponse.error)
|
||||
}
|
||||
|
||||
input.draftReviewService.registerDocxOpen({
|
||||
input.fileReviewService.registerDocxOpen({
|
||||
runId: input.runId,
|
||||
docId: openResponse.payload.docId,
|
||||
sourcePath
|
||||
@@ -432,7 +432,7 @@ export function registerHandlers(services: {
|
||||
)
|
||||
}
|
||||
|
||||
input.draftReviewService.registerDocxChanges({
|
||||
input.fileReviewService.registerDocxChanges({
|
||||
runId: input.runId,
|
||||
previousDocId: openResponse.payload.docId,
|
||||
nextDocId: applyAcceptedResponse.payload.newDocId,
|
||||
@@ -457,7 +457,7 @@ export function registerHandlers(services: {
|
||||
)
|
||||
}
|
||||
|
||||
input.draftReviewService.registerDocxChanges({
|
||||
input.fileReviewService.registerDocxChanges({
|
||||
runId: input.runId,
|
||||
previousDocId: applyAcceptedResponse.payload.newDocId,
|
||||
nextDocId: applyRejectedResponse.payload.newDocId,
|
||||
@@ -472,9 +472,9 @@ export function registerHandlers(services: {
|
||||
}
|
||||
}
|
||||
|
||||
const createE2EDraftReviews = async (input: {
|
||||
const createE2EFileReviews = async (input: {
|
||||
runId: string
|
||||
draftReviewService: DraftReviewService
|
||||
fileReviewService: FileReviewService
|
||||
conflict: boolean
|
||||
multiple: boolean
|
||||
primaryDocument?: {
|
||||
@@ -495,9 +495,9 @@ export function registerHandlers(services: {
|
||||
acceptedReplace: input.multiple ? 'E2E accepted paragraph A' : 'E2E accepted paragraph',
|
||||
rejectedReplace: input.multiple ? 'E2E rejected paragraph A' : 'E2E rejected paragraph'
|
||||
}
|
||||
const firstDocument = await stageE2EDraftReviewDocument({
|
||||
const firstDocument = await stageE2EFileReview({
|
||||
runId: input.runId,
|
||||
draftReviewService: input.draftReviewService,
|
||||
fileReviewService: input.fileReviewService,
|
||||
sourceFileName: primaryDocument.sourceFileName,
|
||||
acceptedReplace: primaryDocument.acceptedReplace,
|
||||
rejectedReplace: primaryDocument.rejectedReplace
|
||||
@@ -510,9 +510,9 @@ export function registerHandlers(services: {
|
||||
acceptedReplace: 'E2E accepted paragraph B',
|
||||
rejectedReplace: 'E2E rejected paragraph B'
|
||||
}
|
||||
const secondDocument = await stageE2EDraftReviewDocument({
|
||||
const secondDocument = await stageE2EFileReview({
|
||||
runId: input.runId,
|
||||
draftReviewService: input.draftReviewService,
|
||||
fileReviewService: input.fileReviewService,
|
||||
sourceFileName: secondaryDocument.sourceFileName,
|
||||
acceptedReplace: secondaryDocument.acceptedReplace,
|
||||
rejectedReplace: secondaryDocument.rejectedReplace
|
||||
@@ -520,7 +520,7 @@ export function registerHandlers(services: {
|
||||
stagedSources.push(secondDocument.sourcePath)
|
||||
}
|
||||
|
||||
const details = await input.draftReviewService.finalizeRunReview({
|
||||
const details = await input.fileReviewService.finalizeRunReview({
|
||||
runId: input.runId,
|
||||
workspaceRootPath: firstDocument.workspaceRootPath
|
||||
})
|
||||
@@ -544,7 +544,7 @@ export function registerHandlers(services: {
|
||||
const emitE2EEditReviewFlow = async (input: {
|
||||
runId: string
|
||||
sendEvent: (event: AiRunEvent) => void
|
||||
draftReviewService: DraftReviewService
|
||||
fileReviewService: FileReviewService
|
||||
conflict: boolean
|
||||
multiple: boolean
|
||||
primaryDocument?: {
|
||||
@@ -576,9 +576,9 @@ export function registerHandlers(services: {
|
||||
})
|
||||
await pauseForE2EFixture()
|
||||
|
||||
const details = await createE2EDraftReviews({
|
||||
const details = await createE2EFileReviews({
|
||||
runId: input.runId,
|
||||
draftReviewService: input.draftReviewService,
|
||||
fileReviewService: input.fileReviewService,
|
||||
conflict: input.conflict,
|
||||
multiple: input.multiple,
|
||||
primaryDocument: input.primaryDocument,
|
||||
@@ -586,25 +586,22 @@ export function registerHandlers(services: {
|
||||
})
|
||||
|
||||
details.forEach((detail) => {
|
||||
detail.documents.forEach((document) => {
|
||||
const reviewArtifactPath = getReviewArtifactPath(document)
|
||||
if (!reviewArtifactPath) return
|
||||
input.sendEvent({
|
||||
type: 'file_artifact',
|
||||
runId: input.runId,
|
||||
taskId: input.runId,
|
||||
artifactId: uuidv4(),
|
||||
capability: 'docx',
|
||||
path: reviewArtifactPath,
|
||||
fileName: path.basename(reviewArtifactPath),
|
||||
operation: 'created',
|
||||
artifactRole: 'draft',
|
||||
reviewId: detail.review.id,
|
||||
draftDocumentId: document.id,
|
||||
draftSnapshotId: document.latest_draft_snapshot_id,
|
||||
summary: `review doc ready: ${document.source_name}`,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
const draftArtifactPath = getDraftArtifactPath(detail.review)
|
||||
if (!draftArtifactPath) return
|
||||
input.sendEvent({
|
||||
type: 'file_artifact',
|
||||
runId: input.runId,
|
||||
taskId: input.runId,
|
||||
artifactId: uuidv4(),
|
||||
capability: 'docx',
|
||||
path: draftArtifactPath,
|
||||
fileName: path.basename(draftArtifactPath),
|
||||
operation: 'created',
|
||||
artifactRole: 'draft',
|
||||
reviewId: detail.review.id,
|
||||
draftSnapshotId: detail.review.latest_draft_snapshot_id,
|
||||
summary: `review doc ready: ${detail.review.source_name}`,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
})
|
||||
await pauseForE2EFixture()
|
||||
@@ -631,7 +628,7 @@ export function registerHandlers(services: {
|
||||
runId: input.runId,
|
||||
review: {
|
||||
reviewId: detail.review.id,
|
||||
documentCount: detail.review.document_count,
|
||||
documentCount: 1,
|
||||
changeCount: detail.review.change_count
|
||||
},
|
||||
createdAt: Date.now()
|
||||
@@ -640,25 +637,22 @@ export function registerHandlers(services: {
|
||||
await pauseForE2EFixture()
|
||||
|
||||
details.forEach((detail) => {
|
||||
detail.documents.forEach((document) => {
|
||||
const workingArtifactPath = getWorkingArtifactPath(document)
|
||||
if (!workingArtifactPath) return
|
||||
input.sendEvent({
|
||||
type: 'file_artifact',
|
||||
runId: input.runId,
|
||||
taskId: input.runId,
|
||||
artifactId: uuidv4(),
|
||||
capability: 'docx',
|
||||
path: workingArtifactPath,
|
||||
fileName: path.basename(workingArtifactPath),
|
||||
operation: 'created',
|
||||
artifactRole: 'draft',
|
||||
reviewId: detail.review.id,
|
||||
draftDocumentId: document.id,
|
||||
draftSnapshotId: document.latest_draft_snapshot_id,
|
||||
summary: `draft ready: ${document.source_name}`,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
const workingArtifactPath = getWorkingArtifactPath(detail.review)
|
||||
if (!workingArtifactPath) return
|
||||
input.sendEvent({
|
||||
type: 'file_artifact',
|
||||
runId: input.runId,
|
||||
taskId: input.runId,
|
||||
artifactId: uuidv4(),
|
||||
capability: 'docx',
|
||||
path: workingArtifactPath,
|
||||
fileName: path.basename(workingArtifactPath),
|
||||
operation: 'created',
|
||||
artifactRole: 'draft',
|
||||
reviewId: detail.review.id,
|
||||
draftSnapshotId: detail.review.latest_draft_snapshot_id,
|
||||
summary: `draft ready: ${detail.review.source_name}`,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
})
|
||||
input.sendEvent({
|
||||
@@ -744,7 +738,7 @@ export function registerHandlers(services: {
|
||||
policyProfile?: string
|
||||
}
|
||||
sendEvent: (event: AiRunEvent) => void
|
||||
draftReviewService: DraftReviewService
|
||||
fileReviewService: FileReviewService
|
||||
}): Promise<boolean> => {
|
||||
if (!isE2EFixtureModeEnabled()) return false
|
||||
|
||||
@@ -842,7 +836,7 @@ export function registerHandlers(services: {
|
||||
await emitE2EEditReviewFlow({
|
||||
runId: input.runId,
|
||||
sendEvent: input.sendEvent,
|
||||
draftReviewService: input.draftReviewService,
|
||||
fileReviewService: input.fileReviewService,
|
||||
conflict: false,
|
||||
multiple: false
|
||||
})
|
||||
@@ -853,7 +847,7 @@ export function registerHandlers(services: {
|
||||
await emitE2EEditReviewFlow({
|
||||
runId: input.runId,
|
||||
sendEvent: input.sendEvent,
|
||||
draftReviewService: input.draftReviewService,
|
||||
fileReviewService: input.fileReviewService,
|
||||
conflict: false,
|
||||
multiple: false,
|
||||
primaryDocument: {
|
||||
@@ -870,7 +864,7 @@ export function registerHandlers(services: {
|
||||
await emitE2EEditReviewFlow({
|
||||
runId: input.runId,
|
||||
sendEvent: input.sendEvent,
|
||||
draftReviewService: input.draftReviewService,
|
||||
fileReviewService: input.fileReviewService,
|
||||
conflict: false,
|
||||
multiple: true
|
||||
})
|
||||
@@ -881,7 +875,7 @@ export function registerHandlers(services: {
|
||||
await emitE2EEditReviewFlow({
|
||||
runId: input.runId,
|
||||
sendEvent: input.sendEvent,
|
||||
draftReviewService: input.draftReviewService,
|
||||
fileReviewService: input.fileReviewService,
|
||||
conflict: true,
|
||||
multiple: false
|
||||
})
|
||||
@@ -2108,7 +2102,7 @@ export function registerHandlers(services: {
|
||||
}
|
||||
|
||||
sendEvent({ type: 'run_started', runId, sessionId, createdAt: Date.now(), input })
|
||||
draftReviewService.beginRun({
|
||||
fileReviewService.beginRun({
|
||||
runId,
|
||||
sessionId,
|
||||
inputText: input
|
||||
@@ -2120,7 +2114,7 @@ export function registerHandlers(services: {
|
||||
inputText: input,
|
||||
workspace,
|
||||
sendEvent,
|
||||
draftReviewService
|
||||
fileReviewService
|
||||
})
|
||||
if (handledByE2EFixture) {
|
||||
return
|
||||
@@ -2148,7 +2142,7 @@ export function registerHandlers(services: {
|
||||
modelService
|
||||
})
|
||||
},
|
||||
draftReviewService,
|
||||
fileReviewService,
|
||||
isCancelled: () => Boolean(activeAiRuns.get(runId)?.cancelled),
|
||||
sendEvent,
|
||||
recordLlmIo: services.recordLlmIo,
|
||||
@@ -2237,13 +2231,13 @@ export function registerHandlers(services: {
|
||||
)
|
||||
|
||||
if (result.status === 'cancelled') {
|
||||
draftReviewService.discardRun(runId)
|
||||
fileReviewService.discardRun(runId)
|
||||
sendEvent({ type: 'run_cancelled', runId })
|
||||
return
|
||||
}
|
||||
|
||||
if (result.status === 'failed') {
|
||||
draftReviewService.discardRun(runId)
|
||||
fileReviewService.discardRun(runId)
|
||||
sendEvent({ type: 'run_failed', runId, error: result.error || 'failed' })
|
||||
return
|
||||
}
|
||||
@@ -2266,7 +2260,7 @@ export function registerHandlers(services: {
|
||||
|
||||
sendEvent({ type: 'run_completed', runId })
|
||||
} catch (e: unknown) {
|
||||
draftReviewService.discardRun(runId)
|
||||
fileReviewService.discardRun(runId)
|
||||
if (activeAiRuns.get(runId)?.cancelled) {
|
||||
sendEvent({ type: 'run_cancelled', runId })
|
||||
return
|
||||
@@ -2326,10 +2320,10 @@ export function registerHandlers(services: {
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'draft-review-list',
|
||||
async (_event, payload?: ListDraftReviewsRequest): Promise<ListDraftReviewsResponse> => {
|
||||
'file-review-list',
|
||||
async (_event, payload?: ListFileReviewsRequest): Promise<ListFileReviewsResponse> => {
|
||||
try {
|
||||
return await draftReviewService.listReviews(payload)
|
||||
return await fileReviewService.listReviews(payload)
|
||||
} catch (e: unknown) {
|
||||
console.error('[DRAFT-REVIEW-LIST] Error:', e)
|
||||
return { items: [], total: 0 }
|
||||
@@ -2338,10 +2332,10 @@ export function registerHandlers(services: {
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'draft-review-get',
|
||||
async (_event, payload: GetDraftReviewRequest): Promise<GetDraftReviewResponse> => {
|
||||
'file-review-get',
|
||||
async (_event, payload: GetFileReviewRequest): Promise<GetFileReviewResponse> => {
|
||||
try {
|
||||
return await draftReviewService.getReview(payload)
|
||||
return await fileReviewService.getReview(payload)
|
||||
} catch (e: unknown) {
|
||||
console.error('[DRAFT-REVIEW-GET] Error:', e)
|
||||
return { error: getErrorMessage(e) }
|
||||
@@ -2350,10 +2344,10 @@ export function registerHandlers(services: {
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'draft-review-validate',
|
||||
async (_event, payload: ValidateDraftCommitRequest): Promise<ValidateDraftCommitResponse> => {
|
||||
'file-review-validate',
|
||||
async (_event, payload: ValidateFileReviewCommitRequest): Promise<ValidateFileReviewCommitResponse> => {
|
||||
try {
|
||||
return await draftReviewService.validateCommit(payload)
|
||||
return await fileReviewService.validateCommit(payload)
|
||||
} catch (e: unknown) {
|
||||
console.error('[DRAFT-REVIEW-VALIDATE] Error:', e)
|
||||
return {
|
||||
@@ -2370,10 +2364,10 @@ export function registerHandlers(services: {
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'draft-review-commit',
|
||||
async (_event, payload: CommitDraftReviewRequest): Promise<CommitDraftReviewResponse> => {
|
||||
'file-review-commit',
|
||||
async (_event, payload: CommitFileReviewRequest): Promise<CommitFileReviewResponse> => {
|
||||
try {
|
||||
return await draftReviewService.commitReview(payload)
|
||||
return await fileReviewService.commitReview(payload)
|
||||
} catch (e: unknown) {
|
||||
console.error('[DRAFT-REVIEW-COMMIT] Error:', e)
|
||||
return { success: false, error: getErrorMessage(e) }
|
||||
@@ -2382,10 +2376,10 @@ export function registerHandlers(services: {
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'draft-review-discard',
|
||||
async (_event, payload: DiscardDraftReviewRequest): Promise<DiscardDraftReviewResponse> => {
|
||||
'file-review-discard',
|
||||
async (_event, payload: DiscardFileReviewRequest): Promise<DiscardFileReviewResponse> => {
|
||||
try {
|
||||
return await draftReviewService.discardReview(payload)
|
||||
return await fileReviewService.discardReview(payload)
|
||||
} catch (e: unknown) {
|
||||
console.error('[DRAFT-REVIEW-DISCARD] Error:', e)
|
||||
return { success: false, error: getErrorMessage(e) }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { END, START, StateGraph } from '@langchain/langgraph'
|
||||
import type { AiRunContext, HumanRunDispatchOptions } from '../../types'
|
||||
import { appendDraftReviewArtifactsAndEmitEvents } from '../../utils/artifacts'
|
||||
import type { HumanState, HumanTaskInput, HumanTaskOutput } from '../types'
|
||||
import { createBrainNode } from '../node/brainNode'
|
||||
import { createLearningNode } from '../node/learningNode'
|
||||
@@ -69,24 +68,12 @@ export const createHumanGraphRunner = (input: { ctx: AiRunContext }) => {
|
||||
recursionLimit: RECURSION_LIMIT
|
||||
})) as HumanState
|
||||
|
||||
const draftReviewDetails = await ctx.finalizeDocxReviewRun()
|
||||
const artifacts =
|
||||
draftReviewDetails.length > 0
|
||||
? appendDraftReviewArtifactsAndEmitEvents({
|
||||
existingArtifacts: output.result?.artifacts,
|
||||
details: draftReviewDetails,
|
||||
sendEvent: ctx.sendEvent,
|
||||
runId: ctx.runId,
|
||||
taskId: taskInput.taskId
|
||||
})
|
||||
: output.result?.artifacts
|
||||
|
||||
return {
|
||||
taskId: taskInput.taskId,
|
||||
status: output.result?.status || 'completed',
|
||||
summary: output.result?.summary || '任务未产出最终结果。',
|
||||
deliveryMarkdown: output.result?.deliveryMarkdown || '',
|
||||
artifacts
|
||||
artifacts: output.result?.artifacts
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.discardDocxReviewRun()
|
||||
|
||||
@@ -3,6 +3,11 @@ import type { HumanRuntimeCache, HumanState } from '../../types'
|
||||
import { buildMouthPrompt } from '../../prompts/mouth'
|
||||
import { completeHumanTask } from '../utils/stateTransitions'
|
||||
import { deriveDeliverySummary } from '../../shared/deliverySummary'
|
||||
import {
|
||||
appendFileReviewArtifactsAndEmitEvents,
|
||||
buildRuntimeArtifactsSummaryFromFileReviewDetails,
|
||||
mergeArtifactRefs
|
||||
} from '../../../utils/artifacts'
|
||||
|
||||
export const createMouthNode = (input: {
|
||||
ctx: AiRunContext
|
||||
@@ -13,10 +18,32 @@ export const createMouthNode = (input: {
|
||||
input.ctx.checkCancelled()
|
||||
input.cache.currentState = state
|
||||
|
||||
const prompt = buildMouthPrompt({ state })
|
||||
const fileReviewDetails = await input.ctx.finalizeDocxReviewRun()
|
||||
const runtimeArtifacts = buildRuntimeArtifactsSummaryFromFileReviewDetails(fileReviewDetails)
|
||||
const artifacts =
|
||||
fileReviewDetails.length > 0
|
||||
? appendFileReviewArtifactsAndEmitEvents({
|
||||
existingArtifacts: state.execution.artifacts,
|
||||
details: fileReviewDetails,
|
||||
sendEvent: input.ctx.sendEvent,
|
||||
runId: input.ctx.runId,
|
||||
taskId: state.task.taskId
|
||||
})
|
||||
: mergeArtifactRefs(state.execution.artifacts)
|
||||
const readyState: HumanState = {
|
||||
...state,
|
||||
execution: {
|
||||
...state.execution,
|
||||
artifacts,
|
||||
runtimeArtifacts
|
||||
}
|
||||
}
|
||||
input.cache.currentState = readyState
|
||||
|
||||
const prompt = buildMouthPrompt({ state: readyState, ctx: input.ctx })
|
||||
const rawDeliveryMarkdown = await input.ctx.chatText({
|
||||
caller: 'human.mouth',
|
||||
taskId: state.task.taskId,
|
||||
taskId: readyState.task.taskId,
|
||||
system: prompt.system,
|
||||
user: prompt.user,
|
||||
schemaName: 'human_mouth_markdown',
|
||||
@@ -26,7 +53,7 @@ export const createMouthNode = (input: {
|
||||
input.ctx.sendEvent({
|
||||
type: 'answer_token',
|
||||
runId: input.ctx.runId,
|
||||
taskId: state.task.taskId,
|
||||
taskId: readyState.task.taskId,
|
||||
token: text,
|
||||
stage: 'human_mouth'
|
||||
})
|
||||
@@ -36,16 +63,16 @@ export const createMouthNode = (input: {
|
||||
|
||||
const deliveryMarkdown =
|
||||
String(rawDeliveryMarkdown || '').trim() ||
|
||||
['# 处理结果', '', state.execution.currentDecision?.decision.summary || '任务结束'].join('\n')
|
||||
['# 处理结果', '', readyState.execution.currentDecision?.decision.summary || '任务结束'].join('\n')
|
||||
const summary = deriveDeliverySummary({
|
||||
deliveryMarkdown,
|
||||
state
|
||||
state: readyState
|
||||
})
|
||||
|
||||
return completeHumanTask({
|
||||
ctx: input.ctx,
|
||||
cache: input.cache,
|
||||
state,
|
||||
state: readyState,
|
||||
summary,
|
||||
deliveryMarkdown,
|
||||
emitAnswerEvents: input.emitAnswerEvents
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { AiRunContext } from '../../types'
|
||||
import { getLoadedHistoryContext } from '../memory/roundHistory'
|
||||
import { renderUnifiedHistoryText } from '../shared/historyRenderers'
|
||||
import { renderHumanStableNotes } from '../shared/memoryRenderers'
|
||||
import { renderHumanArtifactLines } from '../shared/runtimeRenderers'
|
||||
import { renderReferenceScopeText } from '../../prompts/referenceScope'
|
||||
import type { HumanState } from '../types'
|
||||
|
||||
export const buildMouthPrompt = (input: { state: HumanState }) => {
|
||||
export const buildMouthPrompt = (input: { state: HumanState; ctx?: AiRunContext }) => {
|
||||
const system = `
|
||||
<system>
|
||||
<principles>
|
||||
@@ -13,18 +15,19 @@ export const buildMouthPrompt = (input: { state: HumanState }) => {
|
||||
|
||||
<context_information>
|
||||
- task_goal:用户的诉求。
|
||||
- reference_scope:当前工作区、默认资料范围、显式选择的优先参考资料与当前缩圈状态。
|
||||
- your_notes:当前持续工作的任务笔记。其中:
|
||||
- 当前理解:当前任务的最新长期理解。
|
||||
- 关键依据:支撑这些理解或下一步动作的证据、来源与关键结果。
|
||||
- 检查清单:当前任务板,帮助你理解哪些事项已完成、未完成、受阻或已废弃。
|
||||
- history:任务过程中真实发生过的决策、执行和结果,可用于补足 your_notes 不够具体的地方。
|
||||
- runtime_artifacts:运行中已经产生的真实产物,可按给定路径、文件名、状态和摘要引用。
|
||||
- runtime_artifacts:当前这次运行已经形成的文件改动结果摘要,可用于判断实际改动了哪些文件。
|
||||
</context_information>
|
||||
|
||||
<output_contract>
|
||||
- 只输出最终 markdown 正文,不要输出 JSON 或额外说明。
|
||||
- 回答面向用户,但内容必须忠实于当前整体状态,不能脱离已经成立的信息。
|
||||
- 优先吸收 your_notes、history、runtime_artifacts 中已经成立的信息,再组织成自然回复。
|
||||
- 优先吸收 reference_scope、your_notes、history、runtime_artifacts 中已经成立的信息,再组织成自然回复。
|
||||
- 不要遗漏会影响用户理解当前任务状态的关键信息。
|
||||
- 如果需要回复不在上下文中的内容时,在结尾单独加一行“补充说明:...”明确指出这些点来自模型通用知识而非当前本地资料。
|
||||
</output_contract>
|
||||
@@ -37,6 +40,17 @@ export const buildMouthPrompt = (input: { state: HumanState }) => {
|
||||
${input.state.task.userInput}
|
||||
</task_goal>
|
||||
|
||||
<reference_scope>
|
||||
${renderReferenceScopeText(
|
||||
input.ctx?.referenceScope || {
|
||||
selectedReferences: [],
|
||||
hasExplicitSelectedReferences: false,
|
||||
hasDirectDocumentNarrowing: false,
|
||||
defaultKnowledgeScope: 'unavailable'
|
||||
}
|
||||
)}
|
||||
</reference_scope>
|
||||
|
||||
<your_notes>
|
||||
${renderHumanStableNotes(input.state, 'mouth_prompt')}
|
||||
</your_notes>
|
||||
|
||||
@@ -184,6 +184,20 @@ export const humanDeliveryPayloadSchema = z.object({
|
||||
|
||||
export const mouthOutputSchema = humanDeliveryPayloadSchema
|
||||
|
||||
export const runtimeChangedFileSchema = z.object({
|
||||
file_name: z.string().min(1),
|
||||
source_path: z.string().min(1),
|
||||
format: z.literal('docx'),
|
||||
change_count: z.number().int().min(0),
|
||||
summary: z.string().min(1),
|
||||
change_highlights: z.array(z.string().min(1)),
|
||||
draft_artifact_path: z.string().min(1).optional()
|
||||
})
|
||||
|
||||
export const runtimeArtifactsSummarySchema = z.object({
|
||||
changed_files: z.array(runtimeChangedFileSchema)
|
||||
})
|
||||
|
||||
export const humanResultSchema = humanDeliveryPayloadSchema.extend({
|
||||
status: z.literal('completed'),
|
||||
artifacts: z.array(aiArtifactRefSchema).optional()
|
||||
@@ -202,7 +216,8 @@ export const humanExecutionSchema = z.object({
|
||||
watchHint: humanWatchHintSchema.optional(),
|
||||
lastToolResults: z.array(toolCallResultSchema).optional(),
|
||||
lastLearningOutcome: humanLearningOutcomeSchema.optional(),
|
||||
artifacts: z.array(aiArtifactRefSchema)
|
||||
artifacts: z.array(aiArtifactRefSchema),
|
||||
runtimeArtifacts: runtimeArtifactsSummarySchema.optional()
|
||||
})
|
||||
|
||||
export const humanRoundExecutionSchema = z.discriminatedUnion('kind', [
|
||||
|
||||
@@ -4,37 +4,51 @@ import { renderLineList } from '../../utils/render'
|
||||
export type HumanRuntimeRenderMode = 'prompt' | 'fallback_markdown'
|
||||
|
||||
const getRenderFallback = (mode: HumanRuntimeRenderMode): string =>
|
||||
mode === 'fallback_markdown' ? '无' : 'none'
|
||||
mode === 'fallback_markdown' ? '无' : '本次运行未形成新的文件改动结果。'
|
||||
|
||||
const getRenderStyle = (mode: HumanRuntimeRenderMode): 'plain' | 'bullet' =>
|
||||
mode === 'fallback_markdown' ? 'bullet' : 'plain'
|
||||
|
||||
const renderArtifactLine = (
|
||||
artifact: HumanState['execution']['artifacts'][number],
|
||||
const renderChangedFile = (
|
||||
file: NonNullable<HumanState['execution']['runtimeArtifacts']>['changed_files'][number],
|
||||
mode: HumanRuntimeRenderMode
|
||||
): string => {
|
||||
if (artifact.kind === 'file') {
|
||||
const roleText = artifact.artifactRole ? `;role=${artifact.artifactRole}` : ''
|
||||
if (mode === 'prompt') {
|
||||
return `- 文件 ${artifact.fileName}:path=${artifact.path};capability=${artifact.capability};operation=${artifact.operation}${roleText};summary=${artifact.summary}`
|
||||
}
|
||||
const highlights =
|
||||
file.change_highlights.length > 0
|
||||
? file.change_highlights.map((item) => ` - ${item}`).join('\n')
|
||||
: ' - 未提供更细的改动标题'
|
||||
|
||||
const roleLabel = artifact.artifactRole ? `/${artifact.artifactRole}` : ''
|
||||
return `${artifact.fileName} (${artifact.path}) [${artifact.capability}/${artifact.operation}${roleLabel}] - ${artifact.summary}`
|
||||
if (mode === 'fallback_markdown') {
|
||||
return `${file.file_name} (${file.source_path}) - ${file.summary}`
|
||||
}
|
||||
|
||||
if (mode === 'prompt') {
|
||||
return `- draft review ${artifact.review.reviewId}:documents=${artifact.review.documentCount};changes=${artifact.review.changeCount};summary=${artifact.summary}`
|
||||
}
|
||||
|
||||
return `draft review ${artifact.review.reviewId} (${artifact.review.documentCount} docs, ${artifact.review.changeCount} changes) - ${artifact.summary}`
|
||||
const draftLine = file.draft_artifact_path
|
||||
? ` - 待确认稿:${file.draft_artifact_path}`
|
||||
: undefined
|
||||
return [
|
||||
`- 文件:${file.file_name}`,
|
||||
` - 路径:${file.source_path}`,
|
||||
` - 格式:${file.format}`,
|
||||
` - 改动数:${file.change_count}`,
|
||||
` - 摘要:${file.summary}`,
|
||||
' - 大致改动:',
|
||||
highlights,
|
||||
draftLine
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
export const renderHumanArtifactLines = (
|
||||
state: HumanState,
|
||||
mode: HumanRuntimeRenderMode
|
||||
): string =>
|
||||
renderLineList(state.execution.artifacts.map((artifact) => renderArtifactLine(artifact, mode)), {
|
||||
fallback: getRenderFallback(mode),
|
||||
style: getRenderStyle(mode)
|
||||
})
|
||||
renderLineList(
|
||||
(state.execution.runtimeArtifacts?.changed_files || []).map((file) =>
|
||||
renderChangedFile(file, mode)
|
||||
),
|
||||
{
|
||||
fallback: getRenderFallback(mode),
|
||||
style: getRenderStyle(mode)
|
||||
}
|
||||
)
|
||||
@@ -1,326 +0,0 @@
|
||||
import { randomUUID, createHash } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type {
|
||||
CommitDraftReviewRequest,
|
||||
CommitDraftReviewResponse,
|
||||
DraftCommitJob,
|
||||
DraftCommitResultItem,
|
||||
DraftCommitValidationIssue,
|
||||
DraftReviewDocument,
|
||||
DraftReviewSummary,
|
||||
ValidateDraftCommitRequest,
|
||||
ValidateDraftCommitResponse
|
||||
} from '../../../../../../../shared/types'
|
||||
import type { DraftReviewStore } from '../../../../../storage/domains/DraftReviewStore'
|
||||
import {
|
||||
collectArtifactPaths,
|
||||
createArchivedCommittedDetail,
|
||||
isSourceVersionFresh
|
||||
} from './utils'
|
||||
import { getCommittedViewPath, getReviewArtifactPath, getWorkingArtifactPath } from './viewProjection'
|
||||
|
||||
const buildTempOutputPath = (sourcePath: string, suffix: string): string =>
|
||||
path.join(path.dirname(sourcePath), `${path.basename(sourcePath)}.${suffix}.tmp`)
|
||||
|
||||
const fileExists = async (filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const readFileStats = async (
|
||||
filePath: string
|
||||
): Promise<{ checksum: string; byteSize: number }> => {
|
||||
const buffer = await fs.readFile(filePath)
|
||||
return {
|
||||
checksum: createHash('sha256').update(buffer).digest('hex'),
|
||||
byteSize: buffer.byteLength
|
||||
}
|
||||
}
|
||||
|
||||
export class DraftReviewCommitService {
|
||||
public constructor(private readonly draftReviewStore: DraftReviewStore) {}
|
||||
|
||||
public async validateCommit(
|
||||
request: ValidateDraftCommitRequest
|
||||
): Promise<ValidateDraftCommitResponse> {
|
||||
const detail = await this.draftReviewStore.getReviewDetail(request.reviewId)
|
||||
if (!detail) {
|
||||
return {
|
||||
ready: false,
|
||||
issues: [
|
||||
{
|
||||
code: 'session_not_found',
|
||||
message: '审阅稿不存在或已被移除。'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const issues: DraftCommitValidationIssue[] = []
|
||||
const status = detail.review.status
|
||||
if (
|
||||
status === 'committing' ||
|
||||
status === 'committed' ||
|
||||
status === 'discarded' ||
|
||||
status === 'superseded'
|
||||
) {
|
||||
issues.push({
|
||||
code: 'session_not_ready',
|
||||
message: '当前审阅稿状态不允许提交。'
|
||||
})
|
||||
}
|
||||
|
||||
const documentIssues = await Promise.all(
|
||||
detail.documents.map(async (document) => await this.validateDocument(detail.review.id, document))
|
||||
)
|
||||
documentIssues.forEach((documentIssueList) => {
|
||||
issues.push(...documentIssueList)
|
||||
})
|
||||
|
||||
return {
|
||||
ready: issues.length === 0,
|
||||
issues
|
||||
}
|
||||
}
|
||||
|
||||
public async commitReview(
|
||||
request: CommitDraftReviewRequest
|
||||
): Promise<CommitDraftReviewResponse> {
|
||||
const detail = await this.draftReviewStore.getReviewDetail(request.reviewId)
|
||||
if (!detail) return { success: false, error: 'review session not found' }
|
||||
if (!request.overwrite) {
|
||||
return { success: false, error: 'overwrite confirmation is required' }
|
||||
}
|
||||
|
||||
const validation = await this.validateCommit({ reviewId: request.reviewId })
|
||||
if (!validation.ready) {
|
||||
return await this.handleValidationFailure({
|
||||
request,
|
||||
review: detail.review,
|
||||
documentCount: detail.documents.length,
|
||||
issues: validation.issues
|
||||
})
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const job: DraftCommitJob = {
|
||||
id: randomUUID(),
|
||||
review_id: request.reviewId,
|
||||
status: 'running',
|
||||
document_count: detail.documents.length,
|
||||
results: [],
|
||||
started_at: now
|
||||
}
|
||||
await this.appendCommitJob(request.reviewId, job)
|
||||
await this.updateReviewSummary({
|
||||
...detail.review,
|
||||
status: 'committing',
|
||||
updated_at: now,
|
||||
last_error: undefined
|
||||
})
|
||||
|
||||
const results: DraftCommitResultItem[] = []
|
||||
const tempPaths: string[] = []
|
||||
|
||||
try {
|
||||
for (const document of detail.documents) {
|
||||
const committedViewPath = getCommittedViewPath(document)
|
||||
if (!committedViewPath) {
|
||||
throw new Error(`missing committed view for ${document.source_name}`)
|
||||
}
|
||||
const sourceTempPath = buildTempOutputPath(document.source_path, `commit-${job.id}`)
|
||||
tempPaths.push(sourceTempPath)
|
||||
|
||||
const committedBuffer = await fs.readFile(committedViewPath)
|
||||
await fs.writeFile(sourceTempPath, committedBuffer)
|
||||
await fs.rename(sourceTempPath, document.source_path)
|
||||
|
||||
const stats = await readFileStats(document.source_path)
|
||||
results.push({
|
||||
draft_document_id: document.id,
|
||||
output_path: document.source_path,
|
||||
operation: 'updated',
|
||||
checksum: stats.checksum,
|
||||
byte_size: stats.byteSize
|
||||
})
|
||||
}
|
||||
|
||||
const completedJob: DraftCommitJob = {
|
||||
...job,
|
||||
status: 'completed',
|
||||
results,
|
||||
completed_at: Date.now()
|
||||
}
|
||||
const committedAt = completedJob.completed_at || Date.now()
|
||||
const archivedDetail = createArchivedCommittedDetail({
|
||||
detail,
|
||||
completedJob,
|
||||
committedAt
|
||||
})
|
||||
await this.draftReviewStore.saveReviewDetail(archivedDetail)
|
||||
await Promise.all(
|
||||
[...collectArtifactPaths(detail), ...tempPaths].map(async (artifactPath) => {
|
||||
try {
|
||||
await fs.rm(artifactPath, { force: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
success: true,
|
||||
job: completedJob
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
const failedJob: DraftCommitJob = {
|
||||
...job,
|
||||
status: 'failed',
|
||||
results,
|
||||
completed_at: Date.now(),
|
||||
error: message
|
||||
}
|
||||
await this.replaceCommitJob(request.reviewId, failedJob)
|
||||
await this.updateReviewSummary({
|
||||
...detail.review,
|
||||
status: 'failed',
|
||||
updated_at: Date.now(),
|
||||
last_error: message
|
||||
})
|
||||
await Promise.all(
|
||||
tempPaths.map(async (artifactPath) => {
|
||||
try {
|
||||
await fs.rm(artifactPath, { force: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
success: false,
|
||||
job: failedJob,
|
||||
error: message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async validateDocument(
|
||||
_reviewId: string,
|
||||
document: DraftReviewDocument
|
||||
): Promise<DraftCommitValidationIssue[]> {
|
||||
const issues: DraftCommitValidationIssue[] = []
|
||||
const reviewPath = getReviewArtifactPath(document)
|
||||
const workingPath = getWorkingArtifactPath(document)
|
||||
const committedPath = getCommittedViewPath(document)
|
||||
|
||||
if (!reviewPath) {
|
||||
issues.push({
|
||||
code: 'review_artifact_missing',
|
||||
message: '审阅稿不存在,无法提交。',
|
||||
documentId: document.id,
|
||||
sourcePath: document.source_path
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
if (!(await fileExists(reviewPath))) {
|
||||
issues.push({
|
||||
code: 'review_artifact_missing',
|
||||
message: '审阅稿已丢失,需要重新生成。',
|
||||
documentId: document.id,
|
||||
sourcePath: document.source_path
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
if (!workingPath || !(await fileExists(workingPath))) {
|
||||
issues.push({
|
||||
code: 'working_artifact_missing',
|
||||
message: '当前工作稿已丢失,需要重新生成。',
|
||||
documentId: document.id,
|
||||
sourcePath: document.source_path
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
if (!committedPath || !(await fileExists(committedPath))) {
|
||||
issues.push({
|
||||
code: 'committed_artifact_missing',
|
||||
message: '最终提交稿已丢失,需要重新生成。',
|
||||
documentId: document.id,
|
||||
sourcePath: document.source_path
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
const fresh = await isSourceVersionFresh(document.source_path, document.source_version)
|
||||
if (!fresh) {
|
||||
issues.push({
|
||||
code: 'source_changed',
|
||||
message: '源文件已发生变化,无法基于旧审阅稿直接替换。',
|
||||
documentId: document.id,
|
||||
sourcePath: document.source_path
|
||||
})
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
private async handleValidationFailure(input: {
|
||||
request: CommitDraftReviewRequest
|
||||
review: DraftReviewSummary
|
||||
documentCount: number
|
||||
issues: DraftCommitValidationIssue[]
|
||||
}): Promise<CommitDraftReviewResponse> {
|
||||
const hasConflict = input.issues.some((issue) => issue.code === 'source_changed')
|
||||
const now = Date.now()
|
||||
const errorMessage = input.issues.map((issue) => issue.message).join(';')
|
||||
let conflictJob: DraftCommitJob | undefined
|
||||
|
||||
if (hasConflict) {
|
||||
conflictJob = {
|
||||
id: randomUUID(),
|
||||
review_id: input.request.reviewId,
|
||||
status: 'conflicted',
|
||||
document_count: input.documentCount,
|
||||
results: [],
|
||||
started_at: now,
|
||||
completed_at: now,
|
||||
error: errorMessage
|
||||
}
|
||||
await this.appendCommitJob(input.request.reviewId, conflictJob)
|
||||
await this.updateReviewSummary({
|
||||
...input.review,
|
||||
status: 'conflicted',
|
||||
updated_at: now,
|
||||
last_error: errorMessage
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
job: conflictJob,
|
||||
error: errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
private async appendCommitJob(reviewId: string, job: DraftCommitJob): Promise<void> {
|
||||
const jobs = await this.draftReviewStore.listCommitJobs(reviewId)
|
||||
await this.draftReviewStore.replaceCommitJobs(reviewId, [...jobs, job])
|
||||
}
|
||||
|
||||
private async replaceCommitJob(reviewId: string, nextJob: DraftCommitJob): Promise<void> {
|
||||
const jobs = await this.draftReviewStore.listCommitJobs(reviewId)
|
||||
const filtered = jobs.filter((job) => job.id !== nextJob.id)
|
||||
await this.draftReviewStore.replaceCommitJobs(reviewId, [...filtered, nextJob])
|
||||
}
|
||||
|
||||
private async updateReviewSummary(review: DraftReviewSummary): Promise<void> {
|
||||
await this.draftReviewStore.saveReview(review)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { randomUUID, createHash } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type {
|
||||
CommitAttempt,
|
||||
CommitResultItem,
|
||||
CommitValidationIssue,
|
||||
CommitFileReviewRequest,
|
||||
CommitFileReviewResponse,
|
||||
FileReview,
|
||||
ValidateFileReviewCommitRequest,
|
||||
ValidateFileReviewCommitResponse
|
||||
} from '../../../../../../../shared/types'
|
||||
import type { FileReviewStore } from '../../../../../storage/domains/FileReviewStore'
|
||||
import {
|
||||
collectArtifactPaths,
|
||||
createArchivedCommittedDetail,
|
||||
isSourceVersionFresh
|
||||
} from './utils'
|
||||
import {
|
||||
getCommittedArtifactPath,
|
||||
getDraftArtifactPath,
|
||||
getWorkingArtifactPath
|
||||
} from './viewProjection'
|
||||
|
||||
const buildTempOutputPath = (sourcePath: string, suffix: string): string =>
|
||||
path.join(path.dirname(sourcePath), `${path.basename(sourcePath)}.${suffix}.tmp`)
|
||||
|
||||
const fileExists = async (filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const readFileStats = async (
|
||||
filePath: string
|
||||
): Promise<{ checksum: string; byteSize: number }> => {
|
||||
const buffer = await fs.readFile(filePath)
|
||||
return {
|
||||
checksum: createHash('sha256').update(buffer).digest('hex'),
|
||||
byteSize: buffer.byteLength
|
||||
}
|
||||
}
|
||||
|
||||
export class FileReviewCommitService {
|
||||
public constructor(private readonly fileReviewStore: FileReviewStore) {}
|
||||
|
||||
public async validateCommit(
|
||||
request: ValidateFileReviewCommitRequest
|
||||
): Promise<ValidateFileReviewCommitResponse> {
|
||||
const detail = await this.fileReviewStore.getReviewDetail(request.reviewId)
|
||||
if (!detail) {
|
||||
return {
|
||||
ready: false,
|
||||
issues: [{ code: 'session_not_found', message: '审阅稿不存在或已被移除。' }]
|
||||
}
|
||||
}
|
||||
|
||||
const issues: CommitValidationIssue[] = []
|
||||
const status = detail.review.status
|
||||
if (
|
||||
status === 'committing' ||
|
||||
status === 'committed' ||
|
||||
status === 'discarded' ||
|
||||
status === 'superseded'
|
||||
) {
|
||||
issues.push({
|
||||
code: 'session_not_ready',
|
||||
message: '当前审阅稿状态不允许提交。'
|
||||
})
|
||||
}
|
||||
|
||||
issues.push(...(await this.validateReview(detail.review)))
|
||||
return { ready: issues.length === 0, issues }
|
||||
}
|
||||
|
||||
public async commitReview(
|
||||
request: CommitFileReviewRequest
|
||||
): Promise<CommitFileReviewResponse> {
|
||||
const detail = await this.fileReviewStore.getReviewDetail(request.reviewId)
|
||||
if (!detail) return { success: false, error: 'review session not found' }
|
||||
if (!request.overwrite) {
|
||||
return { success: false, error: 'overwrite confirmation is required' }
|
||||
}
|
||||
|
||||
const validation = await this.validateCommit({ reviewId: request.reviewId })
|
||||
if (!validation.ready) {
|
||||
return await this.handleValidationFailure({
|
||||
request,
|
||||
review: detail.review,
|
||||
issues: validation.issues
|
||||
})
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const attempt: CommitAttempt = {
|
||||
id: randomUUID(),
|
||||
review_id: request.reviewId,
|
||||
status: 'running',
|
||||
results: [],
|
||||
started_at: now
|
||||
}
|
||||
await this.appendCommitAttempt(request.reviewId, attempt)
|
||||
await this.updateReviewSummary({
|
||||
...detail.review,
|
||||
status: 'committing',
|
||||
updated_at: now,
|
||||
last_error: undefined
|
||||
})
|
||||
|
||||
const tempPaths: string[] = []
|
||||
|
||||
try {
|
||||
const committedArtifactPath = getCommittedArtifactPath(detail.review)
|
||||
if (!committedArtifactPath) {
|
||||
throw new Error(`missing committed view for ${detail.review.source_name}`)
|
||||
}
|
||||
const sourceTempPath = buildTempOutputPath(detail.review.source_path, `commit-${attempt.id}`)
|
||||
tempPaths.push(sourceTempPath)
|
||||
|
||||
const committedBuffer = await fs.readFile(committedArtifactPath)
|
||||
await fs.writeFile(sourceTempPath, committedBuffer)
|
||||
await fs.rename(sourceTempPath, detail.review.source_path)
|
||||
|
||||
const stats = await readFileStats(detail.review.source_path)
|
||||
const results: CommitResultItem[] = [
|
||||
{
|
||||
review_id: detail.review.id,
|
||||
output_path: detail.review.source_path,
|
||||
operation: 'updated',
|
||||
checksum: stats.checksum,
|
||||
byte_size: stats.byteSize
|
||||
}
|
||||
]
|
||||
|
||||
const completedAttempt: CommitAttempt = {
|
||||
...attempt,
|
||||
status: 'completed',
|
||||
results,
|
||||
completed_at: Date.now()
|
||||
}
|
||||
const committedAt = completedAttempt.completed_at || Date.now()
|
||||
const archivedDetail = createArchivedCommittedDetail({
|
||||
detail,
|
||||
completedAttempt,
|
||||
committedAt
|
||||
})
|
||||
await this.fileReviewStore.saveReviewDetail(archivedDetail)
|
||||
await Promise.all(
|
||||
[...collectArtifactPaths(detail), ...tempPaths].map(async (artifactPath) => {
|
||||
try {
|
||||
await fs.rm(artifactPath, { force: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
)
|
||||
return { success: true, job: completedAttempt }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
const failedAttempt: CommitAttempt = {
|
||||
...attempt,
|
||||
status: 'failed',
|
||||
completed_at: Date.now(),
|
||||
error: message
|
||||
}
|
||||
await this.replaceCommitAttempt(request.reviewId, failedAttempt)
|
||||
await this.updateReviewSummary({
|
||||
...detail.review,
|
||||
status: 'failed',
|
||||
updated_at: Date.now(),
|
||||
last_error: message
|
||||
})
|
||||
await Promise.all(
|
||||
tempPaths.map(async (artifactPath) => {
|
||||
try {
|
||||
await fs.rm(artifactPath, { force: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
)
|
||||
return { success: false, job: failedAttempt, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
private async validateReview(review: FileReview): Promise<CommitValidationIssue[]> {
|
||||
const issues: CommitValidationIssue[] = []
|
||||
const draftPath = getDraftArtifactPath(review)
|
||||
const workingPath = getWorkingArtifactPath(review)
|
||||
const committedPath = getCommittedArtifactPath(review)
|
||||
|
||||
if (!draftPath) {
|
||||
issues.push({
|
||||
code: 'review_artifact_missing',
|
||||
message: '审阅稿不存在,无法提交。',
|
||||
sourcePath: review.source_path
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
if (!(await fileExists(draftPath))) {
|
||||
issues.push({
|
||||
code: 'review_artifact_missing',
|
||||
message: '审阅稿已丢失,需要重新生成。',
|
||||
sourcePath: review.source_path
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
if (!workingPath || !(await fileExists(workingPath))) {
|
||||
issues.push({
|
||||
code: 'working_artifact_missing',
|
||||
message: '当前工作稿已丢失,需要重新生成。',
|
||||
sourcePath: review.source_path
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
if (!committedPath || !(await fileExists(committedPath))) {
|
||||
issues.push({
|
||||
code: 'committed_artifact_missing',
|
||||
message: '最终提交稿已丢失,需要重新生成。',
|
||||
sourcePath: review.source_path
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
const fresh = await isSourceVersionFresh(review.source_path, review.source_version)
|
||||
if (!fresh) {
|
||||
issues.push({
|
||||
code: 'source_changed',
|
||||
message: '源文件已发生变化,无法基于旧审阅稿直接替换。',
|
||||
sourcePath: review.source_path
|
||||
})
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
private async handleValidationFailure(input: {
|
||||
request: CommitFileReviewRequest
|
||||
review: FileReview
|
||||
issues: CommitValidationIssue[]
|
||||
}): Promise<CommitFileReviewResponse> {
|
||||
const hasConflict = input.issues.some((issue) => issue.code === 'source_changed')
|
||||
const now = Date.now()
|
||||
const errorMessage = input.issues.map((issue) => issue.message).join(';')
|
||||
let conflictAttempt: CommitAttempt | undefined
|
||||
|
||||
if (hasConflict) {
|
||||
conflictAttempt = {
|
||||
id: randomUUID(),
|
||||
review_id: input.request.reviewId,
|
||||
status: 'conflicted',
|
||||
results: [],
|
||||
started_at: now,
|
||||
completed_at: now,
|
||||
error: errorMessage
|
||||
}
|
||||
await this.appendCommitAttempt(input.request.reviewId, conflictAttempt)
|
||||
await this.updateReviewSummary({
|
||||
...input.review,
|
||||
status: 'conflicted',
|
||||
updated_at: now,
|
||||
last_error: errorMessage
|
||||
})
|
||||
}
|
||||
|
||||
return { success: false, job: conflictAttempt, error: errorMessage }
|
||||
}
|
||||
|
||||
private async appendCommitAttempt(reviewId: string, nextAttempt: CommitAttempt): Promise<void> {
|
||||
const attempts = await this.fileReviewStore.listCommitAttempts(reviewId)
|
||||
await this.fileReviewStore.replaceCommitAttempts(reviewId, [...attempts, nextAttempt])
|
||||
}
|
||||
|
||||
private async replaceCommitAttempt(reviewId: string, nextAttempt: CommitAttempt): Promise<void> {
|
||||
const attempts = await this.fileReviewStore.listCommitAttempts(reviewId)
|
||||
const filtered = attempts.filter((attempt) => attempt.id !== nextAttempt.id)
|
||||
await this.fileReviewStore.replaceCommitAttempts(reviewId, [...filtered, nextAttempt])
|
||||
}
|
||||
|
||||
private async updateReviewSummary(review: FileReview): Promise<void> {
|
||||
await this.fileReviewStore.saveReview(review)
|
||||
}
|
||||
}
|
||||
+47
-50
@@ -1,35 +1,40 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import type {
|
||||
CommitDraftReviewRequest,
|
||||
CommitDraftReviewResponse,
|
||||
DiscardDraftReviewRequest,
|
||||
DiscardDraftReviewResponse,
|
||||
DraftReviewDetail,
|
||||
GetDraftReviewRequest,
|
||||
GetDraftReviewResponse,
|
||||
ListDraftReviewsRequest,
|
||||
ListDraftReviewsResponse,
|
||||
ValidateDraftCommitRequest,
|
||||
ValidateDraftCommitResponse
|
||||
CommitFileReviewRequest,
|
||||
CommitFileReviewResponse,
|
||||
DiscardFileReviewRequest,
|
||||
DiscardFileReviewResponse,
|
||||
FileReviewDetail,
|
||||
GetFileReviewRequest,
|
||||
GetFileReviewResponse,
|
||||
ListFileReviewsRequest,
|
||||
ListFileReviewsResponse,
|
||||
ValidateFileReviewCommitRequest,
|
||||
ValidateFileReviewCommitResponse
|
||||
} from '../../../../../../../shared/types'
|
||||
import type { DocumentsStore } from '../../../../../storage/domains/DocumentsStore'
|
||||
import type { DraftReviewStore } from '../../../../../storage/domains/DraftReviewStore'
|
||||
import { DraftReviewCommitService } from './DraftReviewCommitService'
|
||||
import type { FileReviewStore } from '../../../../../storage/domains/FileReviewStore'
|
||||
import { FileReviewCommitService } from './FileReviewCommitService'
|
||||
import { RunDraftTracker } from './RunDraftTracker'
|
||||
import type { ResolvedDocxOpenTarget } from './types'
|
||||
import { collectArtifactPaths } from './utils'
|
||||
import { getWorkingArtifactPath } from './viewProjection'
|
||||
|
||||
export class DraftReviewService {
|
||||
/**
|
||||
* FileReviewService currently materializes docx reviews only.
|
||||
* Future editable formats should reuse the same FileReview/CommitAttempt outer
|
||||
* model while keeping their own tracker, serializer and commit validation logic.
|
||||
*/
|
||||
export class FileReviewService {
|
||||
private readonly runDraftTracker: RunDraftTracker
|
||||
private readonly commitService: DraftReviewCommitService
|
||||
private readonly commitService: FileReviewCommitService
|
||||
|
||||
public constructor(
|
||||
private readonly draftReviewStore: DraftReviewStore,
|
||||
private readonly fileReviewStore: FileReviewStore,
|
||||
documentsStore: DocumentsStore
|
||||
) {
|
||||
this.runDraftTracker = new RunDraftTracker(documentsStore)
|
||||
this.commitService = new DraftReviewCommitService(draftReviewStore)
|
||||
this.commitService = new FileReviewCommitService(fileReviewStore)
|
||||
}
|
||||
|
||||
public beginRun(input: { runId: string; sessionId: string; inputText: string }): void {
|
||||
@@ -64,48 +69,45 @@ export class DraftReviewService {
|
||||
public async finalizeRunReview(input: {
|
||||
runId: string
|
||||
workspaceRootPath: string
|
||||
}): Promise<DraftReviewDetail[]> {
|
||||
}): Promise<FileReviewDetail[]> {
|
||||
const details = await this.runDraftTracker.finalizeRunReview(input)
|
||||
if (details.length === 0) return []
|
||||
for (const detail of details) {
|
||||
const sourcePath = detail.documents[0]?.source_path
|
||||
if (sourcePath) {
|
||||
await this.supersedeActiveReviews(sourcePath, detail.review.id)
|
||||
}
|
||||
await this.draftReviewStore.saveReviewDetail(detail)
|
||||
await this.supersedeActiveReviews(detail.review.source_path, detail.review.id)
|
||||
await this.fileReviewStore.saveReviewDetail(detail)
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
public async listReviews(
|
||||
request?: ListDraftReviewsRequest
|
||||
): Promise<ListDraftReviewsResponse> {
|
||||
return await this.draftReviewStore.listReviews(request)
|
||||
request?: ListFileReviewsRequest
|
||||
): Promise<ListFileReviewsResponse> {
|
||||
return await this.fileReviewStore.listReviews(request)
|
||||
}
|
||||
|
||||
public async getReview(
|
||||
request: GetDraftReviewRequest
|
||||
): Promise<GetDraftReviewResponse> {
|
||||
const review = await this.draftReviewStore.getReviewDetail(request.reviewId)
|
||||
request: GetFileReviewRequest
|
||||
): Promise<GetFileReviewResponse> {
|
||||
const review = await this.fileReviewStore.getReviewDetail(request.reviewId)
|
||||
return review ? { review } : { error: 'review session not found' }
|
||||
}
|
||||
|
||||
public async validateCommit(
|
||||
request: ValidateDraftCommitRequest
|
||||
): Promise<ValidateDraftCommitResponse> {
|
||||
request: ValidateFileReviewCommitRequest
|
||||
): Promise<ValidateFileReviewCommitResponse> {
|
||||
return await this.commitService.validateCommit(request)
|
||||
}
|
||||
|
||||
public async commitReview(
|
||||
request: CommitDraftReviewRequest
|
||||
): Promise<CommitDraftReviewResponse> {
|
||||
request: CommitFileReviewRequest
|
||||
): Promise<CommitFileReviewResponse> {
|
||||
return await this.commitService.commitReview(request)
|
||||
}
|
||||
|
||||
public async discardReview(
|
||||
request: DiscardDraftReviewRequest
|
||||
): Promise<DiscardDraftReviewResponse> {
|
||||
const detail = await this.draftReviewStore.getReviewDetail(request.reviewId)
|
||||
request: DiscardFileReviewRequest
|
||||
): Promise<DiscardFileReviewResponse> {
|
||||
const detail = await this.fileReviewStore.getReviewDetail(request.reviewId)
|
||||
if (!detail) return { success: false, error: 'review session not found' }
|
||||
if (
|
||||
detail.review.status === 'committing' ||
|
||||
@@ -119,6 +121,9 @@ export class DraftReviewService {
|
||||
...detail.review,
|
||||
status: 'discarded' as const,
|
||||
updated_at: Date.now(),
|
||||
draft_artifact_path: undefined,
|
||||
working_artifact_path: undefined,
|
||||
committed_artifact_path: undefined,
|
||||
last_error: undefined
|
||||
}
|
||||
await Promise.all(
|
||||
@@ -130,16 +135,9 @@ export class DraftReviewService {
|
||||
}
|
||||
})
|
||||
)
|
||||
await this.draftReviewStore.saveReviewDetail({
|
||||
await this.fileReviewStore.saveReviewDetail({
|
||||
...detail,
|
||||
review: updatedReview,
|
||||
documents: detail.documents.map((document) => ({
|
||||
...document,
|
||||
review_artifact_path: undefined,
|
||||
working_artifact_path: undefined,
|
||||
committed_artifact_path: undefined,
|
||||
updated_at: updatedReview.updated_at
|
||||
}))
|
||||
review: updatedReview
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
@@ -148,7 +146,7 @@ export class DraftReviewService {
|
||||
}
|
||||
|
||||
public async resolveDocxOpenTarget(sourcePath: string): Promise<ResolvedDocxOpenTarget> {
|
||||
const activeReviews = await this.draftReviewStore.listActiveReviewsBySourcePath(sourcePath, [
|
||||
const activeReviews = await this.fileReviewStore.listActiveReviewsBySourcePath(sourcePath, [
|
||||
'pending_confirm',
|
||||
'conflicted'
|
||||
])
|
||||
@@ -160,8 +158,7 @@ export class DraftReviewService {
|
||||
}
|
||||
}
|
||||
|
||||
const document = activeDetail.documents.find((item) => item.source_path === sourcePath)
|
||||
const workingArtifactPath = document ? getWorkingArtifactPath(document) : undefined
|
||||
const workingArtifactPath = getWorkingArtifactPath(activeDetail.review)
|
||||
if (!workingArtifactPath) {
|
||||
throw new Error('active working draft is missing')
|
||||
}
|
||||
@@ -175,13 +172,13 @@ export class DraftReviewService {
|
||||
sourcePath: string,
|
||||
nextReviewId: string
|
||||
): Promise<void> {
|
||||
const activeReviews = await this.draftReviewStore.listActiveReviewsBySourcePath(sourcePath, [
|
||||
const activeReviews = await this.fileReviewStore.listActiveReviewsBySourcePath(sourcePath, [
|
||||
'pending_confirm',
|
||||
'conflicted'
|
||||
])
|
||||
for (const detail of activeReviews) {
|
||||
if (detail.review.id === nextReviewId) continue
|
||||
await this.draftReviewStore.saveReviewDetail({
|
||||
await this.fileReviewStore.saveReviewDetail({
|
||||
...detail,
|
||||
review: {
|
||||
...detail.review,
|
||||
@@ -2,10 +2,9 @@ import { randomUUID } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type {
|
||||
DraftReviewChange,
|
||||
DraftReviewDetail,
|
||||
DraftReviewDocument,
|
||||
DraftReviewSummary
|
||||
FileReview,
|
||||
FileReviewChange,
|
||||
FileReviewDetail
|
||||
} from '../../../../../../../shared/types'
|
||||
import type { DocumentsStore } from '../../../../../storage/domains/DocumentsStore'
|
||||
import type { DocxChangedBlock, DocxCreatedBlock, DocxOp, DocxReplaySession } from '../types'
|
||||
@@ -87,7 +86,6 @@ export class RunDraftTracker {
|
||||
|
||||
const sourcePath = state.sourcePathByDocId.get(input.previousDocId)
|
||||
if (!sourcePath) return
|
||||
|
||||
const document = state.documentBySourcePath.get(sourcePath)
|
||||
if (!document) return
|
||||
|
||||
@@ -100,7 +98,7 @@ export class RunDraftTracker {
|
||||
public async finalizeRunReview(input: {
|
||||
runId: string
|
||||
workspaceRootPath: string
|
||||
}): Promise<DraftReviewDetail[]> {
|
||||
}): Promise<FileReviewDetail[]> {
|
||||
const state = this.runDrafts.get(input.runId)
|
||||
if (!state) return []
|
||||
|
||||
@@ -113,20 +111,19 @@ export class RunDraftTracker {
|
||||
}
|
||||
|
||||
const client = getDocxWorkerClient()
|
||||
const details: DraftReviewDetail[] = []
|
||||
const details: FileReviewDetail[] = []
|
||||
|
||||
for (const [index, document] of changedDocuments.entries()) {
|
||||
for (const document of changedDocuments) {
|
||||
const reviewId = randomUUID()
|
||||
const now = Date.now()
|
||||
const { reviewArtifactPath, workingArtifactPath, committedArtifactPath } =
|
||||
const { draftArtifactPath, workingArtifactPath, committedArtifactPath } =
|
||||
buildDocxReviewArtifactPaths({
|
||||
workspaceRootPath: input.workspaceRootPath,
|
||||
reviewId,
|
||||
documentIndex: index,
|
||||
sourceName: document.sourceName
|
||||
})
|
||||
|
||||
const effectiveSaveResponse = await client.call({
|
||||
const saveResponse = await client.call({
|
||||
runId: input.runId,
|
||||
action: 'materialize_draft',
|
||||
payload: {
|
||||
@@ -134,42 +131,33 @@ export class RunDraftTracker {
|
||||
outPath: workingArtifactPath
|
||||
}
|
||||
})
|
||||
if (
|
||||
!effectiveSaveResponse.ok ||
|
||||
effectiveSaveResponse.payload.action !== 'materialize_draft'
|
||||
) {
|
||||
if (!saveResponse.ok || saveResponse.payload.action !== 'materialize_draft') {
|
||||
throw new Error(
|
||||
effectiveSaveResponse.ok
|
||||
? 'draft materialization worker protocol mismatch'
|
||||
: effectiveSaveResponse.error
|
||||
saveResponse.ok ? 'draft materialization worker protocol mismatch' : saveResponse.error
|
||||
)
|
||||
}
|
||||
|
||||
const sourceDocument = await this.documentsStore.getDocumentByFilePath(document.sourcePath)
|
||||
const sourceVersion = await buildSourceVersion(document.sourcePath)
|
||||
const documentId = randomUUID()
|
||||
const sourceBuffer = await fs.readFile(document.sourcePath)
|
||||
const latestDraftBuffer = await fs.readFile(workingArtifactPath)
|
||||
const replaySession = document.replaySession
|
||||
const reviewChanges = replaySession
|
||||
? this.buildReviewChanges({
|
||||
reviewId,
|
||||
documentId,
|
||||
session: replaySession,
|
||||
now
|
||||
})
|
||||
: []
|
||||
|
||||
if (!replaySession || reviewChanges.length === 0) {
|
||||
continue
|
||||
}
|
||||
if (!replaySession || reviewChanges.length === 0) continue
|
||||
|
||||
await materializeDocxProjectedArtifact({
|
||||
sourceBuffer,
|
||||
workingBuffer: latestDraftBuffer,
|
||||
session: replaySession,
|
||||
kind: 'review',
|
||||
outPath: reviewArtifactPath
|
||||
outPath: draftArtifactPath
|
||||
})
|
||||
await materializeDocxProjectedArtifact({
|
||||
sourceBuffer,
|
||||
@@ -178,6 +166,7 @@ export class RunDraftTracker {
|
||||
kind: 'committed',
|
||||
outPath: committedArtifactPath
|
||||
})
|
||||
|
||||
replaySession.viewArtifacts.working = {
|
||||
kind: 'working',
|
||||
path: workingArtifactPath,
|
||||
@@ -185,7 +174,7 @@ export class RunDraftTracker {
|
||||
}
|
||||
replaySession.viewArtifacts.review = {
|
||||
kind: 'review',
|
||||
path: reviewArtifactPath,
|
||||
path: draftArtifactPath,
|
||||
createdAt: now
|
||||
}
|
||||
replaySession.viewArtifacts.committed = {
|
||||
@@ -194,33 +183,22 @@ export class RunDraftTracker {
|
||||
createdAt: now
|
||||
}
|
||||
|
||||
const reviewDocument: DraftReviewDocument = {
|
||||
id: documentId,
|
||||
review_id: reviewId,
|
||||
source_document_id: sourceDocument?.id,
|
||||
source_path: document.sourcePath,
|
||||
source_name: document.sourceName,
|
||||
format: 'docx',
|
||||
latest_draft_snapshot_id: document.latestDocId,
|
||||
review_artifact_path: reviewArtifactPath,
|
||||
working_artifact_path: workingArtifactPath,
|
||||
committed_artifact_path: committedArtifactPath,
|
||||
source_version: sourceVersion,
|
||||
total_change_count: reviewChanges.length,
|
||||
pending_change_count: reviewChanges.length,
|
||||
accepted_change_count: 0,
|
||||
rejected_change_count: 0,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
}
|
||||
|
||||
const review: DraftReviewSummary = {
|
||||
const review: FileReview = {
|
||||
id: reviewId,
|
||||
session_id: state.sessionId,
|
||||
run_id: input.runId,
|
||||
status: 'pending_confirm',
|
||||
title: clipText(`审阅稿:${document.sourceName}`, 80) || '审阅稿',
|
||||
document_count: 1,
|
||||
source_document_id: sourceDocument?.id,
|
||||
source_path: document.sourcePath,
|
||||
source_name: document.sourceName,
|
||||
format: 'docx',
|
||||
review_render_kind: 'docx_block',
|
||||
latest_draft_snapshot_id: document.latestDocId,
|
||||
draft_artifact_path: draftArtifactPath,
|
||||
working_artifact_path: workingArtifactPath,
|
||||
committed_artifact_path: committedArtifactPath,
|
||||
source_version: sourceVersion,
|
||||
change_count: reviewChanges.length,
|
||||
pending_change_count: reviewChanges.length,
|
||||
accepted_change_count: 0,
|
||||
@@ -232,9 +210,8 @@ export class RunDraftTracker {
|
||||
|
||||
details.push({
|
||||
review,
|
||||
documents: [reviewDocument],
|
||||
changes: reviewChanges,
|
||||
commit_jobs: []
|
||||
commit_attempts: []
|
||||
})
|
||||
}
|
||||
|
||||
@@ -244,14 +221,15 @@ export class RunDraftTracker {
|
||||
|
||||
private buildReviewChanges(input: {
|
||||
reviewId: string
|
||||
documentId: string
|
||||
session: DocxReplaySession
|
||||
now: number
|
||||
}): DraftReviewChange[] {
|
||||
}): FileReviewChange[] {
|
||||
const revisions = [...input.session.revisionByDocId.values()]
|
||||
.filter(
|
||||
(revision) =>
|
||||
revision.revisionRef !== 'v1' && (revision.ops?.length || 0) > 0 && (revision.changedBlocks?.length || 0) > 0
|
||||
revision.revisionRef !== 'v1' &&
|
||||
(revision.ops?.length || 0) > 0 &&
|
||||
(revision.changedBlocks?.length || 0) > 0
|
||||
)
|
||||
.sort((left, right) => left.createdAt - right.createdAt)
|
||||
|
||||
@@ -259,27 +237,28 @@ export class RunDraftTracker {
|
||||
return revisions.flatMap((revision) =>
|
||||
(revision.changedBlocks || []).map((change, changeIndex) => {
|
||||
const op = revision.ops?.[changeIndex] || revision.ops?.[0]
|
||||
return ({
|
||||
id: `${input.reviewId}-${revision.revisionRef}-${String(orderIndex)}`,
|
||||
review_id: input.reviewId,
|
||||
document_id: input.documentId,
|
||||
order_index: orderIndex++,
|
||||
change_kind: op ? getDocxChangeKind(op) : 'block',
|
||||
title: op ? buildDocxChangeTitle(op, change) : revision.summary || '文档修改',
|
||||
summary: op ? buildDocxChangeSummary(op, change) : revision.summary || '',
|
||||
decision: 'pending' as const,
|
||||
before_text: change.beforeText,
|
||||
after_text: change.afterText,
|
||||
locator: buildDocxChangeLocator(change),
|
||||
metadata: {
|
||||
revision_ref: revision.revisionRef,
|
||||
group_id: revision.revisionRef,
|
||||
...(op ? { docx_op_json: JSON.stringify(op) } : {}),
|
||||
...(revision.createdBlocks ? { created_blocks_json: JSON.stringify(revision.createdBlocks) } : {})
|
||||
},
|
||||
created_at: input.now,
|
||||
updated_at: input.now
|
||||
})
|
||||
return {
|
||||
id: `${input.reviewId}-${revision.revisionRef}-${String(orderIndex)}`,
|
||||
review_id: input.reviewId,
|
||||
order_index: orderIndex++,
|
||||
change_kind: op ? getDocxChangeKind(op) : 'block',
|
||||
title: op ? buildDocxChangeTitle(op, change) : revision.summary || '文档修改',
|
||||
summary: op ? buildDocxChangeSummary(op, change) : revision.summary || '',
|
||||
decision: 'pending',
|
||||
before_text: change.beforeText,
|
||||
after_text: change.afterText,
|
||||
locator: buildDocxChangeLocator(change),
|
||||
metadata: {
|
||||
revision_ref: revision.revisionRef,
|
||||
group_id: revision.revisionRef,
|
||||
...(op ? { docx_op_json: JSON.stringify(op) } : {}),
|
||||
...(revision.createdBlocks
|
||||
? { created_blocks_json: JSON.stringify(revision.createdBlocks) }
|
||||
: {})
|
||||
},
|
||||
created_at: input.now,
|
||||
updated_at: input.now
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import type {
|
||||
DraftCommitJob,
|
||||
DraftReviewChange,
|
||||
DraftReviewDetail,
|
||||
DraftReviewStatus,
|
||||
CommitAttempt,
|
||||
DraftSourceVersion,
|
||||
FileReview,
|
||||
FileReviewChange,
|
||||
FileReviewStatus,
|
||||
JsonObject
|
||||
} from '../../../../../../../shared/types'
|
||||
import { clipText } from '../../../../utils/text'
|
||||
import type { DocxChangedBlock, DocxOp } from '../types'
|
||||
import { getCommittedViewPath, getReviewArtifactPath, getWorkingArtifactPath } from './viewProjection'
|
||||
import {
|
||||
getCommittedArtifactPath,
|
||||
getDraftArtifactPath,
|
||||
getWorkingArtifactPath
|
||||
} from './viewProjection'
|
||||
|
||||
export const sanitizeFileName = (value: string): string => {
|
||||
const normalized = String(value || '').trim()
|
||||
@@ -37,7 +41,7 @@ export const describeDocxOp = (op: DocxOp): string => {
|
||||
}
|
||||
}
|
||||
|
||||
export const getDocxChangeKind = (op: DocxOp): DraftReviewChange['change_kind'] =>
|
||||
export const getDocxChangeKind = (op: DocxOp): FileReviewChange['change_kind'] =>
|
||||
op.type === 'update_table_cell' ? 'table_cell' : 'block'
|
||||
|
||||
export const buildDocxChangeTitle = (op: DocxOp, change: DocxChangedBlock): string => {
|
||||
@@ -57,8 +61,8 @@ export const buildDocxChangeLocator = (change: DocxChangedBlock): JsonObject =>
|
||||
|
||||
export const toReviewStatus = (
|
||||
hasPending: boolean,
|
||||
previous?: DraftReviewStatus
|
||||
): DraftReviewStatus => {
|
||||
previous?: FileReviewStatus
|
||||
): FileReviewStatus => {
|
||||
if (previous === 'committing' || previous === 'committed') return previous
|
||||
if (
|
||||
previous === 'conflicted' ||
|
||||
@@ -111,24 +115,22 @@ export const parseDocxOpFromMetadata = (metadata?: JsonObject): DocxOp | null =>
|
||||
export const getMetadataGroupId = (metadata?: JsonObject): string =>
|
||||
typeof metadata?.group_id === 'string' ? metadata.group_id : ''
|
||||
|
||||
export const collectArtifactPaths = (detail: DraftReviewDetail): string[] => {
|
||||
export const collectArtifactPaths = (detail: { review: FileReview }): string[] => {
|
||||
const unique = new Set<string>()
|
||||
detail.documents.forEach((document) => {
|
||||
const reviewArtifactPath = getReviewArtifactPath(document)
|
||||
const workingArtifactPath = getWorkingArtifactPath(document)
|
||||
const committedArtifactPath = getCommittedViewPath(document)
|
||||
if (reviewArtifactPath) unique.add(reviewArtifactPath)
|
||||
if (workingArtifactPath) unique.add(workingArtifactPath)
|
||||
if (committedArtifactPath) unique.add(committedArtifactPath)
|
||||
})
|
||||
const draftArtifactPath = getDraftArtifactPath(detail.review)
|
||||
const workingArtifactPath = getWorkingArtifactPath(detail.review)
|
||||
const committedArtifactPath = getCommittedArtifactPath(detail.review)
|
||||
if (draftArtifactPath) unique.add(draftArtifactPath)
|
||||
if (workingArtifactPath) unique.add(workingArtifactPath)
|
||||
if (committedArtifactPath) unique.add(committedArtifactPath)
|
||||
return [...unique]
|
||||
}
|
||||
|
||||
export const createArchivedCommittedDetail = (input: {
|
||||
detail: DraftReviewDetail
|
||||
completedJob: DraftCommitJob
|
||||
detail: { review: FileReview; changes: FileReviewChange[]; commit_attempts: CommitAttempt[] }
|
||||
completedAttempt: CommitAttempt
|
||||
committedAt: number
|
||||
}): DraftReviewDetail => {
|
||||
}): { review: FileReview; changes: FileReviewChange[]; commit_attempts: CommitAttempt[] } => {
|
||||
const updatedAt = input.committedAt
|
||||
return {
|
||||
review: {
|
||||
@@ -136,22 +138,18 @@ export const createArchivedCommittedDetail = (input: {
|
||||
status: 'committed',
|
||||
committed_at: input.committedAt,
|
||||
updated_at: updatedAt,
|
||||
last_error: undefined
|
||||
},
|
||||
documents: input.detail.documents.map((document) => ({
|
||||
...document,
|
||||
review_artifact_path: undefined,
|
||||
draft_artifact_path: undefined,
|
||||
working_artifact_path: undefined,
|
||||
committed_artifact_path: undefined,
|
||||
updated_at: updatedAt
|
||||
})),
|
||||
last_error: undefined
|
||||
},
|
||||
changes: input.detail.changes.map((change) => ({
|
||||
...change,
|
||||
updated_at: updatedAt
|
||||
})),
|
||||
commit_jobs: [
|
||||
...input.detail.commit_jobs.filter((job) => job.id !== input.completedJob.id),
|
||||
input.completedJob
|
||||
commit_attempts: [
|
||||
...input.detail.commit_attempts.filter((attempt) => attempt.id !== input.completedAttempt.id),
|
||||
input.completedAttempt
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'node:path'
|
||||
import type { DraftReviewDocument } from '../../../../../../../shared/types'
|
||||
import type { FileReview } from '../../../../../../../shared/types'
|
||||
|
||||
const sanitizeProjectedFileName = (value: string): string => {
|
||||
const normalized = String(value || '').trim()
|
||||
@@ -8,7 +8,7 @@ const sanitizeProjectedFileName = (value: string): string => {
|
||||
}
|
||||
|
||||
export type DocxReviewArtifactPaths = {
|
||||
reviewArtifactPath: string
|
||||
draftArtifactPath: string
|
||||
workingArtifactPath: string
|
||||
committedArtifactPath: string
|
||||
}
|
||||
@@ -16,43 +16,30 @@ export type DocxReviewArtifactPaths = {
|
||||
export const buildDocxReviewArtifactPaths = (input: {
|
||||
workspaceRootPath: string
|
||||
reviewId: string
|
||||
documentIndex: number
|
||||
sourceName: string
|
||||
}): DocxReviewArtifactPaths => {
|
||||
const draftDir = path.join(
|
||||
const reviewDir = path.join(
|
||||
path.resolve(input.workspaceRootPath),
|
||||
'.ncurator',
|
||||
'draft-reviews',
|
||||
'file-reviews',
|
||||
input.reviewId
|
||||
)
|
||||
const baseName = sanitizeProjectedFileName(input.sourceName)
|
||||
const reviewArtifactPath = path.join(
|
||||
draftDir,
|
||||
`${String(input.documentIndex + 1).padStart(2, '0')}-review-${baseName}`
|
||||
)
|
||||
const workingArtifactPath = path.join(
|
||||
draftDir,
|
||||
`${String(input.documentIndex + 1).padStart(2, '0')}-working-${baseName}`
|
||||
)
|
||||
const committedArtifactPath = path.join(
|
||||
draftDir,
|
||||
`${String(input.documentIndex + 1).padStart(2, '0')}-committed-${baseName}`
|
||||
)
|
||||
return {
|
||||
reviewArtifactPath,
|
||||
workingArtifactPath,
|
||||
committedArtifactPath
|
||||
draftArtifactPath: path.join(reviewDir, `draft-${baseName}`),
|
||||
workingArtifactPath: path.join(reviewDir, `working-${baseName}`),
|
||||
committedArtifactPath: path.join(reviewDir, `committed-${baseName}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const getReviewArtifactPath = (
|
||||
document: Pick<DraftReviewDocument, 'review_artifact_path'>
|
||||
): string | undefined => document.review_artifact_path
|
||||
export const getDraftArtifactPath = (
|
||||
review: Pick<FileReview, 'draft_artifact_path'>
|
||||
): string | undefined => review.draft_artifact_path
|
||||
|
||||
export const getWorkingArtifactPath = (
|
||||
document: Pick<DraftReviewDocument, 'working_artifact_path'>
|
||||
): string | undefined => document.working_artifact_path
|
||||
review: Pick<FileReview, 'working_artifact_path'>
|
||||
): string | undefined => review.working_artifact_path
|
||||
|
||||
export const getCommittedViewPath = (
|
||||
document: Pick<DraftReviewDocument, 'committed_artifact_path'>
|
||||
): string | undefined => document.committed_artifact_path
|
||||
export const getCommittedArtifactPath = (
|
||||
review: Pick<FileReview, 'committed_artifact_path'>
|
||||
): string | undefined => review.committed_artifact_path
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
humanTaskInputSchema,
|
||||
humanTaskOutputSchema,
|
||||
humanTraceStepSchema,
|
||||
runtimeArtifactsSummarySchema,
|
||||
runtimeChangedFileSchema,
|
||||
humanWatchHintSchema,
|
||||
skillDefinitionBaseSchema,
|
||||
toolCallDiagnosticSchema,
|
||||
@@ -91,6 +93,8 @@ export type HumanTaskInput = z.infer<typeof humanTaskInputSchema>
|
||||
export type HumanTaskOutput = z.infer<typeof humanTaskOutputSchema>
|
||||
export type HumanExecution = z.infer<typeof humanExecutionSchema>
|
||||
export type HumanState = z.infer<typeof humanStateContractSchema>
|
||||
export type RuntimeChangedFile = z.infer<typeof runtimeChangedFileSchema>
|
||||
export type RuntimeArtifactsSummary = z.infer<typeof runtimeArtifactsSummarySchema>
|
||||
export type HumanRoundExecution = z.infer<typeof humanRoundExecutionSchema>
|
||||
export type HumanRoundRecord = z.infer<typeof humanRoundRecordSchema>
|
||||
export type HumanLoadedHistoryContext = z.infer<typeof humanLoadedHistoryContextSchema>
|
||||
|
||||
@@ -87,7 +87,7 @@ export const createAiRunContext = (payload: AiRunStartPayload, deps: AiRunDeps):
|
||||
requestApproval: deps.requestApproval,
|
||||
loadHistory,
|
||||
trackDocxOpen: ({ docId, sourcePath, replaySession }) => {
|
||||
deps.draftReviewService.registerDocxOpen({
|
||||
deps.fileReviewService.registerDocxOpen({
|
||||
runId: payload.runId,
|
||||
docId,
|
||||
sourcePath,
|
||||
@@ -102,7 +102,7 @@ export const createAiRunContext = (payload: AiRunStartPayload, deps: AiRunDeps):
|
||||
createdBlocks,
|
||||
replaySession
|
||||
}) => {
|
||||
deps.draftReviewService.registerDocxChanges({
|
||||
deps.fileReviewService.registerDocxChanges({
|
||||
runId: payload.runId,
|
||||
previousDocId,
|
||||
nextDocId,
|
||||
@@ -113,9 +113,9 @@ export const createAiRunContext = (payload: AiRunStartPayload, deps: AiRunDeps):
|
||||
})
|
||||
},
|
||||
resolveDocxOpenTarget: async (sourcePath) =>
|
||||
await deps.draftReviewService.resolveDocxOpenTarget(sourcePath),
|
||||
await deps.fileReviewService.resolveDocxOpenTarget(sourcePath),
|
||||
beginDocxReviewRun: () => {
|
||||
deps.draftReviewService.beginRun({
|
||||
deps.fileReviewService.beginRun({
|
||||
runId: payload.runId,
|
||||
sessionId: payload.sessionId,
|
||||
inputText: String(payload.input || '')
|
||||
@@ -123,13 +123,13 @@ export const createAiRunContext = (payload: AiRunStartPayload, deps: AiRunDeps):
|
||||
},
|
||||
finalizeDocxReviewRun: async () => {
|
||||
if (!normalizedWorkspace?.rootPath) return []
|
||||
return await deps.draftReviewService.finalizeRunReview({
|
||||
return await deps.fileReviewService.finalizeRunReview({
|
||||
runId: payload.runId,
|
||||
workspaceRootPath: normalizedWorkspace.rootPath
|
||||
})
|
||||
},
|
||||
discardDocxReviewRun: () => {
|
||||
deps.draftReviewService.discardRun(payload.runId)
|
||||
deps.fileReviewService.discardRun(payload.runId)
|
||||
},
|
||||
documentsStore: deps.documentsStore,
|
||||
embeddingService: deps.embeddingService
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { z } from 'zod/v4'
|
||||
import type {
|
||||
AiArtifactRef,
|
||||
AiDraftReviewArtifactRef,
|
||||
AiDraftReviewRef,
|
||||
AiFileReviewArtifactRef,
|
||||
AiFileReviewRef,
|
||||
AiFileArtifactRef
|
||||
} from '../../../shared/types'
|
||||
import { AI_RUN_STATUSES } from './constants'
|
||||
|
||||
export const aiDraftReviewRefSchema: z.ZodType<AiDraftReviewRef> = z.object({
|
||||
export const aiFileReviewRefSchema: z.ZodType<AiFileReviewRef> = z.object({
|
||||
reviewId: z.string().min(1),
|
||||
documentCount: z.number().int().min(0),
|
||||
changeCount: z.number().int().min(0)
|
||||
@@ -26,15 +26,15 @@ export const aiFileArtifactRefSchema: z.ZodType<AiFileArtifactRef> = z.object({
|
||||
summary: z.string().min(1)
|
||||
})
|
||||
|
||||
export const aiDraftReviewArtifactRefSchema: z.ZodType<AiDraftReviewArtifactRef> = z.object({
|
||||
export const aiFileReviewArtifactRefSchema: z.ZodType<AiFileReviewArtifactRef> = z.object({
|
||||
kind: z.literal('draft_review'),
|
||||
review: aiDraftReviewRefSchema,
|
||||
review: aiFileReviewRefSchema,
|
||||
summary: z.string().min(1)
|
||||
})
|
||||
|
||||
export const aiArtifactRefSchema: z.ZodType<AiArtifactRef> = z.union([
|
||||
aiFileArtifactRefSchema,
|
||||
aiDraftReviewArtifactRefSchema
|
||||
aiFileReviewArtifactRefSchema
|
||||
])
|
||||
|
||||
export const aiRunStatusSchema = z.enum(AI_RUN_STATUSES)
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
DocxOp,
|
||||
DocxReplaySession
|
||||
} from './human/tools/docx/types'
|
||||
import type { DraftReviewService } from './human/tools/docx/review/DraftReviewService'
|
||||
import type { FileReviewService } from './human/tools/docx/review/FileReviewService'
|
||||
import type { ResolvedDocxOpenTarget } from './human/tools/docx/review/types'
|
||||
import type { z } from 'zod/v4'
|
||||
import type {
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
AiRunEvent,
|
||||
AiRunEventStage,
|
||||
AiRunStatus,
|
||||
DraftReviewDetail,
|
||||
FileReviewDetail,
|
||||
SelectedReference
|
||||
} from '../../../shared/types'
|
||||
import type { HumanTaskInput, HumanTaskOutput } from './human/types'
|
||||
@@ -89,7 +89,7 @@ export type AiRunDeps = {
|
||||
documentsStore: DocumentsStore
|
||||
embeddingService: EmbeddingService
|
||||
ensureEmbeddingReady: () => Promise<void>
|
||||
draftReviewService: DraftReviewService
|
||||
fileReviewService: FileReviewService
|
||||
isCancelled: () => boolean
|
||||
sendEvent: (event: AiRunEvent) => void
|
||||
recordLlmIo?: (record: AiLlmIoRecord) => void
|
||||
@@ -152,7 +152,7 @@ export type AiRunContext = {
|
||||
}) => void
|
||||
resolveDocxOpenTarget: (sourcePath: string) => Promise<ResolvedDocxOpenTarget>
|
||||
beginDocxReviewRun: () => void
|
||||
finalizeDocxReviewRun: () => Promise<DraftReviewDetail[]>
|
||||
finalizeDocxReviewRun: () => Promise<FileReviewDetail[]>
|
||||
discardDocxReviewRun: () => void
|
||||
documentsStore: DocumentsStore
|
||||
embeddingService: EmbeddingService
|
||||
|
||||
@@ -2,16 +2,16 @@ import path from 'node:path'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import type {
|
||||
AiArtifactRef,
|
||||
AiDraftReviewArtifactRef,
|
||||
AiFileArtifactOperation,
|
||||
AiFileArtifactRef,
|
||||
AiFileReviewArtifactRef,
|
||||
AiRunEvent,
|
||||
DraftReviewDetail,
|
||||
FileReviewDetail,
|
||||
JsonObject,
|
||||
JsonValue
|
||||
JsonValue,
|
||||
RuntimeArtifactsSummary
|
||||
} from '../../../../shared/types'
|
||||
import type { ToolCallResult } from '../human/types'
|
||||
import { getReviewArtifactPath } from '../human/tools/docx/review/viewProjection'
|
||||
|
||||
const isJsonObject = (value: JsonValue | undefined): value is JsonObject => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
||||
@@ -41,7 +41,6 @@ const createFileArtifactRef = (input: {
|
||||
summary: string
|
||||
artifactRole?: AiFileArtifactRef['artifactRole']
|
||||
reviewId?: string
|
||||
draftDocumentId?: string
|
||||
draftSnapshotId?: string
|
||||
}): AiFileArtifactRef => ({
|
||||
kind: 'file',
|
||||
@@ -51,7 +50,6 @@ const createFileArtifactRef = (input: {
|
||||
operation: input.operation,
|
||||
artifactRole: input.artifactRole,
|
||||
reviewId: input.reviewId,
|
||||
draftDocumentId: input.draftDocumentId,
|
||||
draftSnapshotId: input.draftSnapshotId,
|
||||
summary: input.summary
|
||||
})
|
||||
@@ -89,9 +87,7 @@ export const mergeArtifactRefs = (
|
||||
groups.forEach((group) => {
|
||||
group?.forEach((artifact) => {
|
||||
const key = getArtifactKey(artifact)
|
||||
if (merged.has(key)) {
|
||||
merged.delete(key)
|
||||
}
|
||||
if (merged.has(key)) merged.delete(key)
|
||||
merged.set(key, artifact)
|
||||
})
|
||||
})
|
||||
@@ -106,49 +102,44 @@ export const extractArtifactRefsFromToolResults = (
|
||||
): AiArtifactRef[] =>
|
||||
mergeArtifactRefs(...results.map((result) => extractArtifactRefsFromToolResult(result)))
|
||||
|
||||
export const buildRunArtifactsFromDraftReviewDetail = (
|
||||
detail: DraftReviewDetail
|
||||
): AiArtifactRef[] => {
|
||||
const draftReviewArtifact: AiDraftReviewArtifactRef = {
|
||||
export const buildRunArtifactsFromFileReviewDetail = (detail: FileReviewDetail): AiArtifactRef[] => {
|
||||
const reviewArtifact: AiFileReviewArtifactRef = {
|
||||
kind: 'draft_review',
|
||||
review: {
|
||||
reviewId: detail.review.id,
|
||||
documentCount: detail.review.document_count,
|
||||
documentCount: 1,
|
||||
changeCount: detail.review.change_count
|
||||
},
|
||||
summary: `review document ready (${detail.review.document_count} docs, ${detail.review.change_count} changes)`
|
||||
summary: `review document ready (1 doc, ${detail.review.change_count} changes)`
|
||||
}
|
||||
|
||||
const draftArtifacts = detail.documents.flatMap((document) => {
|
||||
const reviewArtifactPath = getReviewArtifactPath(document)
|
||||
if (!reviewArtifactPath) return []
|
||||
return [
|
||||
createFileArtifactRef({
|
||||
capability: 'docx',
|
||||
artifactPath: reviewArtifactPath,
|
||||
operation: 'created',
|
||||
artifactRole: 'draft',
|
||||
reviewId: detail.review.id,
|
||||
draftDocumentId: document.id,
|
||||
draftSnapshotId: document.latest_draft_snapshot_id,
|
||||
summary: `review doc ready: ${document.source_name}`
|
||||
})
|
||||
]
|
||||
})
|
||||
const draftArtifacts = detail.review.draft_artifact_path
|
||||
? [
|
||||
createFileArtifactRef({
|
||||
capability: 'docx',
|
||||
artifactPath: detail.review.draft_artifact_path,
|
||||
operation: 'created',
|
||||
artifactRole: 'draft',
|
||||
reviewId: detail.review.id,
|
||||
draftSnapshotId: detail.review.latest_draft_snapshot_id,
|
||||
summary: `review doc ready: ${detail.review.source_name}`
|
||||
})
|
||||
]
|
||||
: []
|
||||
|
||||
return mergeArtifactRefs([draftReviewArtifact], draftArtifacts)
|
||||
return mergeArtifactRefs([reviewArtifact], draftArtifacts)
|
||||
}
|
||||
|
||||
export const appendDraftReviewArtifactsAndEmitEvents = (input: {
|
||||
export const appendFileReviewArtifactsAndEmitEvents = (input: {
|
||||
existingArtifacts?: readonly AiArtifactRef[]
|
||||
details: readonly DraftReviewDetail[]
|
||||
details: readonly FileReviewDetail[]
|
||||
sendEvent: (event: AiRunEvent) => void
|
||||
runId: string
|
||||
taskId: string
|
||||
}): AiArtifactRef[] => {
|
||||
const nextArtifacts = mergeArtifactRefs(
|
||||
input.existingArtifacts,
|
||||
...input.details.map((detail) => buildRunArtifactsFromDraftReviewDetail(detail))
|
||||
...input.details.map((detail) => buildRunArtifactsFromFileReviewDetail(detail))
|
||||
)
|
||||
|
||||
input.details.forEach((detail) => {
|
||||
@@ -157,33 +148,51 @@ export const appendDraftReviewArtifactsAndEmitEvents = (input: {
|
||||
runId: input.runId,
|
||||
review: {
|
||||
reviewId: detail.review.id,
|
||||
documentCount: detail.review.document_count,
|
||||
documentCount: 1,
|
||||
changeCount: detail.review.change_count
|
||||
},
|
||||
createdAt: Date.now()
|
||||
})
|
||||
|
||||
detail.documents.forEach((document) => {
|
||||
const reviewArtifactPath = getReviewArtifactPath(document)
|
||||
if (!reviewArtifactPath) return
|
||||
input.sendEvent({
|
||||
type: 'file_artifact',
|
||||
runId: input.runId,
|
||||
taskId: input.taskId,
|
||||
artifactId: uuidv4(),
|
||||
capability: 'docx',
|
||||
path: reviewArtifactPath,
|
||||
fileName: path.basename(reviewArtifactPath),
|
||||
operation: 'created',
|
||||
artifactRole: 'draft',
|
||||
reviewId: detail.review.id,
|
||||
draftDocumentId: document.id,
|
||||
draftSnapshotId: document.latest_draft_snapshot_id,
|
||||
summary: `review doc ready: ${document.source_name}`,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
if (!detail.review.draft_artifact_path) return
|
||||
input.sendEvent({
|
||||
type: 'file_artifact',
|
||||
runId: input.runId,
|
||||
taskId: input.taskId,
|
||||
artifactId: uuidv4(),
|
||||
capability: 'docx',
|
||||
path: detail.review.draft_artifact_path,
|
||||
fileName: path.basename(detail.review.draft_artifact_path),
|
||||
operation: 'created',
|
||||
artifactRole: 'draft',
|
||||
reviewId: detail.review.id,
|
||||
draftSnapshotId: detail.review.latest_draft_snapshot_id,
|
||||
summary: `review doc ready: ${detail.review.source_name}`,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
})
|
||||
|
||||
return nextArtifacts
|
||||
}
|
||||
|
||||
/**
|
||||
* RuntimeArtifactsSummary is only for mouth consumption. It intentionally
|
||||
* exposes user-facing changed-file facts instead of internal artifact wiring.
|
||||
*/
|
||||
export const buildRuntimeArtifactsSummaryFromFileReviewDetails = (
|
||||
details: readonly FileReviewDetail[]
|
||||
): RuntimeArtifactsSummary => ({
|
||||
changed_files: details.map((detail) => ({
|
||||
file_name: detail.review.source_name,
|
||||
source_path: detail.review.source_path,
|
||||
format: 'docx',
|
||||
change_count: detail.review.change_count,
|
||||
summary:
|
||||
detail.changes[0]?.summary ||
|
||||
(detail.review.change_count > 0
|
||||
? `已生成 ${detail.review.change_count} 处文档修改`
|
||||
: '已生成待确认稿'),
|
||||
change_highlights: detail.changes.map((change) => change.title).filter(Boolean),
|
||||
draft_artifact_path: detail.review.draft_artifact_path
|
||||
}))
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AiRuntimeSettingsStore } from './domains/AiRuntimeSettingsStore'
|
||||
import { LanceDbCore } from './core/LanceDbCore'
|
||||
import { ChatStore } from './domains/ChatStore'
|
||||
import { DraftReviewStore } from './domains/DraftReviewStore'
|
||||
import { FileReviewStore } from './domains/FileReviewStore'
|
||||
import { DocumentsStore } from './domains/DocumentsStore'
|
||||
import { LlmConfigStore } from './domains/LlmConfigStore'
|
||||
import { WebSourceStore } from './domains/WebSourceStore'
|
||||
@@ -16,7 +16,7 @@ export class StorageService {
|
||||
public readonly documents: DocumentsStore
|
||||
public readonly webSource: WebSourceStore
|
||||
public readonly chat: ChatStore
|
||||
public readonly draftReview: DraftReviewStore
|
||||
public readonly fileReview: FileReviewStore
|
||||
public readonly llm: LlmConfigStore
|
||||
public readonly aiRuntimeSettings: AiRuntimeSettingsStore
|
||||
public readonly workspace: WorkspaceStore
|
||||
@@ -27,7 +27,7 @@ export class StorageService {
|
||||
this.documents = new DocumentsStore(this.core)
|
||||
this.webSource = new WebSourceStore(this.core)
|
||||
this.chat = new ChatStore(this.core)
|
||||
this.draftReview = new DraftReviewStore(this.core)
|
||||
this.fileReview = new FileReviewStore(this.core)
|
||||
this.llm = new LlmConfigStore(this.core)
|
||||
this.aiRuntimeSettings = new AiRuntimeSettingsStore(this.core)
|
||||
this.workspace = new WorkspaceStore(this.core)
|
||||
|
||||
@@ -20,10 +20,9 @@ export const LANCE_TABLES = {
|
||||
CHAT_SESSION: 'chat_session',
|
||||
CHAT_MESSAGE: 'chat_message',
|
||||
CHAT_SESSION_MEMORY: 'chat_session_memory',
|
||||
DRAFT_REVIEW: 'draft_review',
|
||||
DRAFT_REVIEW_DOCUMENT: 'draft_review_document',
|
||||
DRAFT_REVIEW_CHANGE: 'draft_review_change',
|
||||
DRAFT_COMMIT_JOB: 'draft_commit_job',
|
||||
FILE_REVIEW: 'file_review',
|
||||
FILE_REVIEW_CHANGE: 'file_review_change',
|
||||
COMMIT_ATTEMPT: 'commit_attempt',
|
||||
LLM_CONFIG: 'llm_config',
|
||||
AI_RUNTIME_SETTINGS: 'ai_runtime_settings'
|
||||
} as const
|
||||
@@ -305,14 +304,23 @@ export class LanceDbCore {
|
||||
])
|
||||
},
|
||||
{
|
||||
name: LANCE_TABLES.DRAFT_REVIEW,
|
||||
name: LANCE_TABLES.FILE_REVIEW,
|
||||
schema: new arrow.Schema([
|
||||
new arrow.Field('id', new arrow.Utf8()),
|
||||
new arrow.Field('session_id', new arrow.Utf8()),
|
||||
new arrow.Field('run_id', new arrow.Utf8()),
|
||||
new arrow.Field('status', new arrow.Utf8()),
|
||||
new arrow.Field('title', new arrow.Utf8()),
|
||||
new arrow.Field('document_count', new arrow.Int32()),
|
||||
new arrow.Field('source_document_id', new arrow.Utf8(), true),
|
||||
new arrow.Field('source_path', new arrow.Utf8()),
|
||||
new arrow.Field('source_name', new arrow.Utf8()),
|
||||
new arrow.Field('format', new arrow.Utf8()),
|
||||
new arrow.Field('review_render_kind', new arrow.Utf8(), true),
|
||||
new arrow.Field('latest_draft_snapshot_id', new arrow.Utf8()),
|
||||
new arrow.Field('draft_artifact_path', new arrow.Utf8(), true),
|
||||
new arrow.Field('working_artifact_path', new arrow.Utf8(), true),
|
||||
new arrow.Field('committed_artifact_path', new arrow.Utf8(), true),
|
||||
new arrow.Field('source_version_json', new arrow.Utf8()),
|
||||
new arrow.Field('change_count', new arrow.Int32()),
|
||||
new arrow.Field('pending_change_count', new arrow.Int32()),
|
||||
new arrow.Field('accepted_change_count', new arrow.Int32()),
|
||||
@@ -325,54 +333,32 @@ export class LanceDbCore {
|
||||
])
|
||||
},
|
||||
{
|
||||
name: LANCE_TABLES.DRAFT_REVIEW_DOCUMENT,
|
||||
name: LANCE_TABLES.FILE_REVIEW_CHANGE,
|
||||
schema: new arrow.Schema([
|
||||
new arrow.Field('id', new arrow.Utf8()),
|
||||
new arrow.Field('review_id', new arrow.Utf8()),
|
||||
new arrow.Field('source_document_id', new arrow.Utf8(), true),
|
||||
new arrow.Field('source_path', new arrow.Utf8()),
|
||||
new arrow.Field('source_name', new arrow.Utf8()),
|
||||
new arrow.Field('format', new arrow.Utf8()),
|
||||
new arrow.Field('latest_draft_snapshot_id', new arrow.Utf8()),
|
||||
new arrow.Field('review_artifact_path', new arrow.Utf8(), true),
|
||||
new arrow.Field('working_artifact_path', new arrow.Utf8(), true),
|
||||
new arrow.Field('committed_artifact_path', new arrow.Utf8(), true),
|
||||
new arrow.Field('source_version_json', new arrow.Utf8()),
|
||||
new arrow.Field('total_change_count', new arrow.Int32()),
|
||||
new arrow.Field('pending_change_count', new arrow.Int32()),
|
||||
new arrow.Field('accepted_change_count', new arrow.Int32()),
|
||||
new arrow.Field('rejected_change_count', new arrow.Int32()),
|
||||
new arrow.Field('created_at', new arrow.Int64()),
|
||||
new arrow.Field('updated_at', new arrow.Int64())
|
||||
])
|
||||
},
|
||||
{
|
||||
name: LANCE_TABLES.DRAFT_REVIEW_CHANGE,
|
||||
schema: new arrow.Schema([
|
||||
new arrow.Field('id', new arrow.Utf8()),
|
||||
new arrow.Field('review_id', new arrow.Utf8()),
|
||||
new arrow.Field('document_id', new arrow.Utf8()),
|
||||
new arrow.Field('order_index', new arrow.Int32()),
|
||||
new arrow.Field('change_kind', new arrow.Utf8()),
|
||||
new arrow.Field('title', new arrow.Utf8()),
|
||||
new arrow.Field('summary', new arrow.Utf8()),
|
||||
new arrow.Field('decision', new arrow.Utf8()),
|
||||
new arrow.Field('before_text', new arrow.Utf8()),
|
||||
new arrow.Field('after_text', new arrow.Utf8()),
|
||||
new arrow.Field('locator_json', new arrow.Utf8()),
|
||||
new arrow.Field('review_artifact_path', new arrow.Utf8(), true),
|
||||
new arrow.Field('before_text', new arrow.Utf8(), true),
|
||||
new arrow.Field('after_text', new arrow.Utf8(), true),
|
||||
new arrow.Field('anchor_name', new arrow.Utf8(), true),
|
||||
new arrow.Field('locator_json', new arrow.Utf8(), true),
|
||||
new arrow.Field('diff_hunks_json', new arrow.Utf8(), true),
|
||||
new arrow.Field('source_step_ref', new arrow.Utf8(), true),
|
||||
new arrow.Field('metadata_json', new arrow.Utf8(), true),
|
||||
new arrow.Field('created_at', new arrow.Int64()),
|
||||
new arrow.Field('updated_at', new arrow.Int64())
|
||||
])
|
||||
},
|
||||
{
|
||||
name: LANCE_TABLES.DRAFT_COMMIT_JOB,
|
||||
name: LANCE_TABLES.COMMIT_ATTEMPT,
|
||||
schema: new arrow.Schema([
|
||||
new arrow.Field('id', new arrow.Utf8()),
|
||||
new arrow.Field('review_id', new arrow.Utf8()),
|
||||
new arrow.Field('status', new arrow.Utf8()),
|
||||
new arrow.Field('document_count', new arrow.Int32()),
|
||||
new arrow.Field('results_json', new arrow.Utf8()),
|
||||
new arrow.Field('started_at', new arrow.Int64()),
|
||||
new arrow.Field('completed_at', new arrow.Int64(), true),
|
||||
|
||||
@@ -1,510 +0,0 @@
|
||||
import type {
|
||||
DraftChangeDecision,
|
||||
DraftCommitJob,
|
||||
DraftCommitResultItem,
|
||||
DraftReviewChange,
|
||||
DraftReviewDetail,
|
||||
DraftReviewDocument,
|
||||
DraftReviewStatus,
|
||||
DraftReviewSummary,
|
||||
DraftSourceVersion,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
ListDraftReviewsRequest,
|
||||
ListDraftReviewsResponse
|
||||
} from '../../../types/store'
|
||||
import { LanceDbCore, LANCE_TABLES } from '../core/LanceDbCore'
|
||||
|
||||
type PrimitiveRowValue = string | number | boolean | null | undefined
|
||||
|
||||
type DraftWriteRow = Record<string, PrimitiveRowValue>
|
||||
|
||||
type DraftReviewRow = {
|
||||
id?: PrimitiveRowValue
|
||||
session_id?: PrimitiveRowValue
|
||||
run_id?: PrimitiveRowValue
|
||||
status?: PrimitiveRowValue
|
||||
title?: PrimitiveRowValue
|
||||
document_count?: PrimitiveRowValue
|
||||
change_count?: PrimitiveRowValue
|
||||
pending_change_count?: PrimitiveRowValue
|
||||
accepted_change_count?: PrimitiveRowValue
|
||||
rejected_change_count?: PrimitiveRowValue
|
||||
created_at?: PrimitiveRowValue
|
||||
updated_at?: PrimitiveRowValue
|
||||
completed_at?: PrimitiveRowValue
|
||||
committed_at?: PrimitiveRowValue
|
||||
last_error?: PrimitiveRowValue
|
||||
}
|
||||
|
||||
type DraftReviewDocumentRow = {
|
||||
id?: PrimitiveRowValue
|
||||
review_id?: PrimitiveRowValue
|
||||
source_document_id?: PrimitiveRowValue
|
||||
source_path?: PrimitiveRowValue
|
||||
source_name?: PrimitiveRowValue
|
||||
format?: PrimitiveRowValue
|
||||
latest_draft_snapshot_id?: PrimitiveRowValue
|
||||
review_artifact_path?: PrimitiveRowValue
|
||||
working_artifact_path?: PrimitiveRowValue
|
||||
committed_artifact_path?: PrimitiveRowValue
|
||||
source_version_json?: PrimitiveRowValue
|
||||
total_change_count?: PrimitiveRowValue
|
||||
pending_change_count?: PrimitiveRowValue
|
||||
accepted_change_count?: PrimitiveRowValue
|
||||
rejected_change_count?: PrimitiveRowValue
|
||||
created_at?: PrimitiveRowValue
|
||||
updated_at?: PrimitiveRowValue
|
||||
}
|
||||
|
||||
type DraftReviewChangeRow = {
|
||||
id?: PrimitiveRowValue
|
||||
review_id?: PrimitiveRowValue
|
||||
document_id?: PrimitiveRowValue
|
||||
order_index?: PrimitiveRowValue
|
||||
change_kind?: PrimitiveRowValue
|
||||
title?: PrimitiveRowValue
|
||||
summary?: PrimitiveRowValue
|
||||
decision?: PrimitiveRowValue
|
||||
before_text?: PrimitiveRowValue
|
||||
after_text?: PrimitiveRowValue
|
||||
locator_json?: PrimitiveRowValue
|
||||
review_artifact_path?: PrimitiveRowValue
|
||||
metadata_json?: PrimitiveRowValue
|
||||
created_at?: PrimitiveRowValue
|
||||
updated_at?: PrimitiveRowValue
|
||||
}
|
||||
|
||||
type DraftCommitJobRow = {
|
||||
id?: PrimitiveRowValue
|
||||
review_id?: PrimitiveRowValue
|
||||
status?: PrimitiveRowValue
|
||||
document_count?: PrimitiveRowValue
|
||||
results_json?: PrimitiveRowValue
|
||||
started_at?: PrimitiveRowValue
|
||||
completed_at?: PrimitiveRowValue
|
||||
error?: PrimitiveRowValue
|
||||
}
|
||||
|
||||
const toOptionalString = (value: PrimitiveRowValue): string | undefined => {
|
||||
if (value == null) return undefined
|
||||
const next = String(value).trim()
|
||||
return next.length > 0 ? next : undefined
|
||||
}
|
||||
|
||||
const toRequiredString = (value: PrimitiveRowValue): string => String(value ?? '')
|
||||
|
||||
const toOptionalNumber = (value: PrimitiveRowValue): number | undefined => {
|
||||
if (value == null) return undefined
|
||||
const next = Number(value)
|
||||
return Number.isFinite(next) ? next : undefined
|
||||
}
|
||||
|
||||
const toRequiredNumber = (value: PrimitiveRowValue, fallback = 0): number => {
|
||||
const next = Number(value)
|
||||
return Number.isFinite(next) ? next : fallback
|
||||
}
|
||||
|
||||
const parseJsonValue = (value: PrimitiveRowValue): JsonValue | undefined => {
|
||||
const raw = toOptionalString(value)
|
||||
if (!raw) return undefined
|
||||
try {
|
||||
return JSON.parse(raw) as JsonValue
|
||||
} catch (error) {
|
||||
void error
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const parseJsonObject = (value: PrimitiveRowValue): JsonObject | undefined => {
|
||||
const parsed = parseJsonValue(value)
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
|
||||
return parsed
|
||||
}
|
||||
|
||||
const parseSourceVersion = (value: PrimitiveRowValue): DraftSourceVersion => {
|
||||
const parsed = parseJsonObject(value)
|
||||
if (!parsed) return {}
|
||||
return {
|
||||
mtime_ms: typeof parsed.mtime_ms === 'number' ? parsed.mtime_ms : undefined,
|
||||
byte_size: typeof parsed.byte_size === 'number' ? parsed.byte_size : undefined,
|
||||
checksum: typeof parsed.checksum === 'string' ? parsed.checksum : undefined,
|
||||
content_hash: typeof parsed.content_hash === 'string' ? parsed.content_hash : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const parseCommitResults = (value: PrimitiveRowValue): DraftCommitResultItem[] => {
|
||||
const parsed = parseJsonValue(value)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed
|
||||
.filter(
|
||||
(item): item is JsonObject =>
|
||||
Boolean(item) && typeof item === 'object' && !Array.isArray(item)
|
||||
)
|
||||
.map((item) => ({
|
||||
draft_document_id:
|
||||
typeof item.draft_document_id === 'string' ? item.draft_document_id : '',
|
||||
output_path: typeof item.output_path === 'string' ? item.output_path : '',
|
||||
operation: item.operation === 'updated' ? ('updated' as const) : ('created' as const),
|
||||
checksum: typeof item.checksum === 'string' ? item.checksum : undefined,
|
||||
byte_size: typeof item.byte_size === 'number' ? item.byte_size : undefined
|
||||
}))
|
||||
.filter((item) => item.draft_document_id && item.output_path)
|
||||
}
|
||||
|
||||
const normalizeDraftReviewStatus = (value: PrimitiveRowValue): DraftReviewStatus => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
switch (normalized) {
|
||||
case 'pending_confirm':
|
||||
case 'committing':
|
||||
case 'committed':
|
||||
case 'conflicted':
|
||||
case 'discarded':
|
||||
case 'superseded':
|
||||
case 'failed':
|
||||
return normalized
|
||||
default:
|
||||
return 'pending_confirm'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeDraftChangeDecision = (value: PrimitiveRowValue): DraftChangeDecision => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
if (normalized === 'accepted' || normalized === 'rejected') return normalized
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
const normalizeDraftDocumentFormat = (value: PrimitiveRowValue): DraftReviewDocument['format'] => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
switch (normalized) {
|
||||
case 'pdf':
|
||||
case 'xlsx':
|
||||
case 'pptx':
|
||||
case 'md':
|
||||
case 'txt':
|
||||
return normalized
|
||||
default:
|
||||
return 'docx'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeDraftChangeKind = (value: PrimitiveRowValue): DraftReviewChange['change_kind'] => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
switch (normalized) {
|
||||
case 'text_hunk':
|
||||
case 'table_cell':
|
||||
case 'worksheet_range':
|
||||
case 'slide_object':
|
||||
case 'page_region':
|
||||
return normalized
|
||||
default:
|
||||
return 'block'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeDraftCommitStatus = (value: PrimitiveRowValue): DraftCommitJob['status'] => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
switch (normalized) {
|
||||
case 'running':
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
case 'conflicted':
|
||||
return normalized
|
||||
default:
|
||||
return 'pending'
|
||||
}
|
||||
}
|
||||
|
||||
const toDraftReviewSummary = (row: DraftReviewRow): DraftReviewSummary => ({
|
||||
id: toRequiredString(row.id),
|
||||
session_id: toRequiredString(row.session_id),
|
||||
run_id: toRequiredString(row.run_id),
|
||||
status: normalizeDraftReviewStatus(row.status),
|
||||
title: toRequiredString(row.title),
|
||||
document_count: toRequiredNumber(row.document_count),
|
||||
change_count: toRequiredNumber(row.change_count),
|
||||
pending_change_count: toRequiredNumber(row.pending_change_count),
|
||||
accepted_change_count: toRequiredNumber(row.accepted_change_count),
|
||||
rejected_change_count: toRequiredNumber(row.rejected_change_count),
|
||||
created_at: toRequiredNumber(row.created_at),
|
||||
updated_at: toRequiredNumber(row.updated_at),
|
||||
completed_at: toOptionalNumber(row.completed_at),
|
||||
committed_at: toOptionalNumber(row.committed_at),
|
||||
last_error: toOptionalString(row.last_error)
|
||||
})
|
||||
|
||||
const toDraftReviewDocument = (row: DraftReviewDocumentRow): DraftReviewDocument => ({
|
||||
id: toRequiredString(row.id),
|
||||
review_id: toRequiredString(row.review_id),
|
||||
source_document_id: toOptionalString(row.source_document_id),
|
||||
source_path: toRequiredString(row.source_path),
|
||||
source_name: toRequiredString(row.source_name),
|
||||
format: normalizeDraftDocumentFormat(row.format),
|
||||
latest_draft_snapshot_id: toRequiredString(row.latest_draft_snapshot_id),
|
||||
review_artifact_path: toOptionalString(row.review_artifact_path),
|
||||
working_artifact_path: toOptionalString(row.working_artifact_path),
|
||||
committed_artifact_path: toOptionalString(row.committed_artifact_path),
|
||||
source_version: parseSourceVersion(row.source_version_json),
|
||||
total_change_count: toRequiredNumber(row.total_change_count),
|
||||
pending_change_count: toRequiredNumber(row.pending_change_count),
|
||||
accepted_change_count: toRequiredNumber(row.accepted_change_count),
|
||||
rejected_change_count: toRequiredNumber(row.rejected_change_count),
|
||||
created_at: toRequiredNumber(row.created_at),
|
||||
updated_at: toRequiredNumber(row.updated_at)
|
||||
})
|
||||
|
||||
const toDraftReviewChange = (row: DraftReviewChangeRow): DraftReviewChange => ({
|
||||
id: toRequiredString(row.id),
|
||||
review_id: toRequiredString(row.review_id),
|
||||
document_id: toRequiredString(row.document_id),
|
||||
order_index: toRequiredNumber(row.order_index),
|
||||
change_kind: normalizeDraftChangeKind(row.change_kind),
|
||||
title: toRequiredString(row.title),
|
||||
summary: toRequiredString(row.summary),
|
||||
decision: normalizeDraftChangeDecision(row.decision),
|
||||
before_text: toRequiredString(row.before_text),
|
||||
after_text: toRequiredString(row.after_text),
|
||||
anchor_name:
|
||||
parseJsonObject(row.metadata_json) &&
|
||||
typeof parseJsonObject(row.metadata_json)?.anchor_name === 'string'
|
||||
? String(parseJsonObject(row.metadata_json)?.anchor_name)
|
||||
: undefined,
|
||||
locator: parseJsonObject(row.locator_json) || {},
|
||||
metadata: parseJsonObject(row.metadata_json),
|
||||
created_at: toRequiredNumber(row.created_at),
|
||||
updated_at: toRequiredNumber(row.updated_at)
|
||||
})
|
||||
|
||||
const toDraftCommitJob = (row: DraftCommitJobRow): DraftCommitJob => ({
|
||||
id: toRequiredString(row.id),
|
||||
review_id: toRequiredString(row.review_id),
|
||||
status: normalizeDraftCommitStatus(row.status),
|
||||
document_count: toRequiredNumber(row.document_count),
|
||||
results: parseCommitResults(row.results_json),
|
||||
started_at: toRequiredNumber(row.started_at),
|
||||
completed_at: toOptionalNumber(row.completed_at),
|
||||
error: toOptionalString(row.error)
|
||||
})
|
||||
|
||||
const omitUndefined = <T extends DraftWriteRow>(value: T): Partial<T> => {
|
||||
const next: Partial<T> = {}
|
||||
for (const key of Object.keys(value) as Array<keyof T>) {
|
||||
if (value[key] !== undefined) {
|
||||
next[key] = value[key]
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
const toDraftReviewWriteRow = (review: DraftReviewSummary): DraftWriteRow => ({
|
||||
id: review.id,
|
||||
session_id: review.session_id,
|
||||
run_id: review.run_id,
|
||||
status: review.status,
|
||||
title: review.title,
|
||||
document_count: review.document_count,
|
||||
change_count: review.change_count,
|
||||
pending_change_count: review.pending_change_count,
|
||||
accepted_change_count: review.accepted_change_count,
|
||||
rejected_change_count: review.rejected_change_count,
|
||||
created_at: review.created_at,
|
||||
updated_at: review.updated_at,
|
||||
completed_at: review.completed_at,
|
||||
committed_at: review.committed_at,
|
||||
last_error: review.last_error
|
||||
})
|
||||
|
||||
const toDraftReviewDocumentWriteRow = (document: DraftReviewDocument): DraftWriteRow => ({
|
||||
id: document.id,
|
||||
review_id: document.review_id,
|
||||
source_document_id: document.source_document_id,
|
||||
source_path: document.source_path,
|
||||
source_name: document.source_name,
|
||||
format: document.format,
|
||||
latest_draft_snapshot_id: document.latest_draft_snapshot_id,
|
||||
review_artifact_path: document.review_artifact_path,
|
||||
working_artifact_path: document.working_artifact_path,
|
||||
committed_artifact_path: document.committed_artifact_path,
|
||||
source_version_json: JSON.stringify(document.source_version),
|
||||
total_change_count: document.total_change_count,
|
||||
pending_change_count: document.pending_change_count,
|
||||
accepted_change_count: document.accepted_change_count,
|
||||
rejected_change_count: document.rejected_change_count,
|
||||
created_at: document.created_at,
|
||||
updated_at: document.updated_at
|
||||
})
|
||||
|
||||
const toDraftReviewChangeWriteRow = (change: DraftReviewChange): DraftWriteRow => ({
|
||||
id: change.id,
|
||||
review_id: change.review_id,
|
||||
document_id: change.document_id,
|
||||
order_index: change.order_index,
|
||||
change_kind: change.change_kind,
|
||||
title: change.title,
|
||||
summary: change.summary,
|
||||
decision: change.decision,
|
||||
before_text: change.before_text,
|
||||
after_text: change.after_text,
|
||||
locator_json: JSON.stringify(change.locator),
|
||||
review_artifact_path: undefined,
|
||||
metadata_json: JSON.stringify({
|
||||
...(change.metadata || {}),
|
||||
...(change.anchor_name ? { anchor_name: change.anchor_name } : {})
|
||||
}),
|
||||
created_at: change.created_at,
|
||||
updated_at: change.updated_at
|
||||
})
|
||||
|
||||
const toDraftCommitJobWriteRow = (job: DraftCommitJob): DraftWriteRow => ({
|
||||
id: job.id,
|
||||
review_id: job.review_id,
|
||||
status: job.status,
|
||||
document_count: job.document_count,
|
||||
results_json: JSON.stringify(job.results),
|
||||
started_at: job.started_at,
|
||||
completed_at: job.completed_at,
|
||||
error: job.error
|
||||
})
|
||||
|
||||
export class DraftReviewStore {
|
||||
public constructor(private readonly core: LanceDbCore) {}
|
||||
|
||||
public async saveReview(review: DraftReviewSummary): Promise<void> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_REVIEW)
|
||||
await table.delete(`id = '${this.core.escapeSqlString(review.id)}'`)
|
||||
await table.add([omitUndefined(toDraftReviewWriteRow(review))])
|
||||
}
|
||||
|
||||
public async listReviews(filters?: ListDraftReviewsRequest): Promise<ListDraftReviewsResponse> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_REVIEW)
|
||||
const where = this.core.combineWhere([
|
||||
filters?.sessionId
|
||||
? `session_id = '${this.core.escapeSqlString(filters.sessionId)}'`
|
||||
: undefined,
|
||||
filters?.status ? `status = '${this.core.escapeSqlString(filters.status)}'` : undefined
|
||||
])
|
||||
const rows = (await (where ? table.query().where(where) : table.query()).toArray()) as DraftReviewRow[]
|
||||
const items = rows.map(toDraftReviewSummary).sort((a, b) => b.updated_at - a.updated_at)
|
||||
return {
|
||||
items,
|
||||
total: items.length
|
||||
}
|
||||
}
|
||||
|
||||
public async getReview(reviewId: string): Promise<DraftReviewSummary | null> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_REVIEW)
|
||||
const rows = (await table
|
||||
.query()
|
||||
.where(`id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
.limit(1)
|
||||
.toArray()) as DraftReviewRow[]
|
||||
const row = rows[0]
|
||||
return row ? toDraftReviewSummary(row) : null
|
||||
}
|
||||
|
||||
public async replaceDocuments(reviewId: string, documents: DraftReviewDocument[]): Promise<void> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_REVIEW_DOCUMENT)
|
||||
await table.delete(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
if (documents.length === 0) return
|
||||
await table.add(documents.map((document) => omitUndefined(toDraftReviewDocumentWriteRow(document))))
|
||||
}
|
||||
|
||||
public async listDocuments(reviewId: string): Promise<DraftReviewDocument[]> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_REVIEW_DOCUMENT)
|
||||
const rows = (await table
|
||||
.query()
|
||||
.where(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
.toArray()) as DraftReviewDocumentRow[]
|
||||
return rows.map(toDraftReviewDocument).sort((a, b) => a.created_at - b.created_at)
|
||||
}
|
||||
|
||||
public async replaceChanges(reviewId: string, changes: DraftReviewChange[]): Promise<void> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_REVIEW_CHANGE)
|
||||
await table.delete(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
if (changes.length === 0) return
|
||||
await table.add(changes.map((change) => omitUndefined(toDraftReviewChangeWriteRow(change))))
|
||||
}
|
||||
|
||||
public async listChanges(reviewId: string): Promise<DraftReviewChange[]> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_REVIEW_CHANGE)
|
||||
const rows = (await table
|
||||
.query()
|
||||
.where(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
.toArray()) as DraftReviewChangeRow[]
|
||||
return rows.map(toDraftReviewChange).sort((a, b) => a.order_index - b.order_index)
|
||||
}
|
||||
|
||||
public async replaceCommitJobs(reviewId: string, jobs: DraftCommitJob[]): Promise<void> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_COMMIT_JOB)
|
||||
await table.delete(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
if (jobs.length === 0) return
|
||||
await table.add(jobs.map((job) => omitUndefined(toDraftCommitJobWriteRow(job))))
|
||||
}
|
||||
|
||||
public async listCommitJobs(reviewId: string): Promise<DraftCommitJob[]> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.DRAFT_COMMIT_JOB)
|
||||
const rows = (await table
|
||||
.query()
|
||||
.where(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
.toArray()) as DraftCommitJobRow[]
|
||||
return rows.map(toDraftCommitJob).sort((a, b) => a.started_at - b.started_at)
|
||||
}
|
||||
|
||||
public async saveReviewDetail(detail: DraftReviewDetail): Promise<void> {
|
||||
await this.saveReview(detail.review)
|
||||
await this.replaceDocuments(detail.review.id, detail.documents)
|
||||
await this.replaceChanges(detail.review.id, detail.changes)
|
||||
await this.replaceCommitJobs(detail.review.id, detail.commit_jobs)
|
||||
}
|
||||
|
||||
public async getReviewDetail(reviewId: string): Promise<DraftReviewDetail | null> {
|
||||
const review = await this.getReview(reviewId)
|
||||
if (!review) return null
|
||||
const [documents, changes, commitJobs] = await Promise.all([
|
||||
this.listDocuments(reviewId),
|
||||
this.listChanges(reviewId),
|
||||
this.listCommitJobs(reviewId)
|
||||
])
|
||||
return {
|
||||
review,
|
||||
documents,
|
||||
changes,
|
||||
commit_jobs: commitJobs
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteReview(reviewId: string): Promise<void> {
|
||||
const safeReviewId = this.core.escapeSqlString(reviewId)
|
||||
const [reviewTable, documentTable, changeTable, commitJobTable] = await Promise.all([
|
||||
this.core.openTable(LANCE_TABLES.DRAFT_REVIEW),
|
||||
this.core.openTable(LANCE_TABLES.DRAFT_REVIEW_DOCUMENT),
|
||||
this.core.openTable(LANCE_TABLES.DRAFT_REVIEW_CHANGE),
|
||||
this.core.openTable(LANCE_TABLES.DRAFT_COMMIT_JOB)
|
||||
])
|
||||
await reviewTable.delete(`id = '${safeReviewId}'`)
|
||||
await documentTable.delete(`review_id = '${safeReviewId}'`)
|
||||
await changeTable.delete(`review_id = '${safeReviewId}'`)
|
||||
await commitJobTable.delete(`review_id = '${safeReviewId}'`)
|
||||
}
|
||||
|
||||
public async listActiveReviewsBySourcePath(
|
||||
sourcePath: string,
|
||||
statuses: DraftReviewStatus[]
|
||||
): Promise<DraftReviewDetail[]> {
|
||||
if (statuses.length === 0) return []
|
||||
const documentTable = await this.core.openTable(LANCE_TABLES.DRAFT_REVIEW_DOCUMENT)
|
||||
const safeSourcePath = this.core.escapeSqlString(sourcePath)
|
||||
const documentRows = (await documentTable
|
||||
.query()
|
||||
.where(`source_path = '${safeSourcePath}'`)
|
||||
.toArray()) as DraftReviewDocumentRow[]
|
||||
const reviewIds = Array.from(
|
||||
new Set(documentRows.map((row) => toOptionalString(row.review_id)).filter(Boolean))
|
||||
) as string[]
|
||||
const details = await Promise.all(reviewIds.map((reviewId) => this.getReviewDetail(reviewId)))
|
||||
return details
|
||||
.filter((detail): detail is DraftReviewDetail => Boolean(detail))
|
||||
.filter((detail) => statuses.includes(detail.review.status))
|
||||
.sort((left, right) => right.review.updated_at - left.review.updated_at)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
import type {
|
||||
CommitAttempt,
|
||||
CommitAttemptStatus,
|
||||
CommitResultItem,
|
||||
DraftChangeDecision,
|
||||
DraftSourceVersion,
|
||||
DraftTextDiffHunk,
|
||||
FileReview,
|
||||
FileReviewChange,
|
||||
FileReviewStatus,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
ListFileReviewsRequest,
|
||||
ListFileReviewsResponse,
|
||||
} from '../../../types/store'
|
||||
import { LanceDbCore, LANCE_TABLES } from '../core/LanceDbCore'
|
||||
|
||||
type PrimitiveRowValue = string | number | boolean | null | undefined
|
||||
type WriteRow = Record<string, PrimitiveRowValue>
|
||||
|
||||
type FileReviewRow = {
|
||||
id?: PrimitiveRowValue
|
||||
session_id?: PrimitiveRowValue
|
||||
run_id?: PrimitiveRowValue
|
||||
status?: PrimitiveRowValue
|
||||
title?: PrimitiveRowValue
|
||||
source_document_id?: PrimitiveRowValue
|
||||
source_path?: PrimitiveRowValue
|
||||
source_name?: PrimitiveRowValue
|
||||
format?: PrimitiveRowValue
|
||||
review_render_kind?: PrimitiveRowValue
|
||||
latest_draft_snapshot_id?: PrimitiveRowValue
|
||||
draft_artifact_path?: PrimitiveRowValue
|
||||
working_artifact_path?: PrimitiveRowValue
|
||||
committed_artifact_path?: PrimitiveRowValue
|
||||
source_version_json?: PrimitiveRowValue
|
||||
change_count?: PrimitiveRowValue
|
||||
pending_change_count?: PrimitiveRowValue
|
||||
accepted_change_count?: PrimitiveRowValue
|
||||
rejected_change_count?: PrimitiveRowValue
|
||||
created_at?: PrimitiveRowValue
|
||||
updated_at?: PrimitiveRowValue
|
||||
completed_at?: PrimitiveRowValue
|
||||
committed_at?: PrimitiveRowValue
|
||||
last_error?: PrimitiveRowValue
|
||||
}
|
||||
|
||||
type FileReviewChangeRow = {
|
||||
id?: PrimitiveRowValue
|
||||
review_id?: PrimitiveRowValue
|
||||
order_index?: PrimitiveRowValue
|
||||
change_kind?: PrimitiveRowValue
|
||||
title?: PrimitiveRowValue
|
||||
summary?: PrimitiveRowValue
|
||||
decision?: PrimitiveRowValue
|
||||
before_text?: PrimitiveRowValue
|
||||
after_text?: PrimitiveRowValue
|
||||
anchor_name?: PrimitiveRowValue
|
||||
locator_json?: PrimitiveRowValue
|
||||
diff_hunks_json?: PrimitiveRowValue
|
||||
source_step_ref?: PrimitiveRowValue
|
||||
metadata_json?: PrimitiveRowValue
|
||||
created_at?: PrimitiveRowValue
|
||||
updated_at?: PrimitiveRowValue
|
||||
}
|
||||
|
||||
type CommitAttemptRow = {
|
||||
id?: PrimitiveRowValue
|
||||
review_id?: PrimitiveRowValue
|
||||
status?: PrimitiveRowValue
|
||||
results_json?: PrimitiveRowValue
|
||||
started_at?: PrimitiveRowValue
|
||||
completed_at?: PrimitiveRowValue
|
||||
error?: PrimitiveRowValue
|
||||
}
|
||||
|
||||
const toOptionalString = (value: PrimitiveRowValue): string | undefined => {
|
||||
if (value == null) return undefined
|
||||
const next = String(value).trim()
|
||||
return next.length > 0 ? next : undefined
|
||||
}
|
||||
|
||||
const toRequiredString = (value: PrimitiveRowValue): string => String(value ?? '')
|
||||
|
||||
const toOptionalNumber = (value: PrimitiveRowValue): number | undefined => {
|
||||
if (value == null) return undefined
|
||||
const next = Number(value)
|
||||
return Number.isFinite(next) ? next : undefined
|
||||
}
|
||||
|
||||
const toRequiredNumber = (value: PrimitiveRowValue, fallback = 0): number => {
|
||||
const next = Number(value)
|
||||
return Number.isFinite(next) ? next : fallback
|
||||
}
|
||||
|
||||
const parseJsonValue = (value: PrimitiveRowValue): JsonValue | undefined => {
|
||||
const raw = toOptionalString(value)
|
||||
if (!raw) return undefined
|
||||
try {
|
||||
return JSON.parse(raw) as JsonValue
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const parseJsonObject = (value: PrimitiveRowValue): JsonObject | undefined => {
|
||||
const parsed = parseJsonValue(value)
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
|
||||
return parsed
|
||||
}
|
||||
|
||||
const parseSourceVersion = (value: PrimitiveRowValue): DraftSourceVersion => {
|
||||
const parsed = parseJsonObject(value)
|
||||
if (!parsed) return {}
|
||||
return {
|
||||
mtime_ms: typeof parsed.mtime_ms === 'number' ? parsed.mtime_ms : undefined,
|
||||
byte_size: typeof parsed.byte_size === 'number' ? parsed.byte_size : undefined,
|
||||
checksum: typeof parsed.checksum === 'string' ? parsed.checksum : undefined,
|
||||
content_hash: typeof parsed.content_hash === 'string' ? parsed.content_hash : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const parseDiffHunks = (value: PrimitiveRowValue): DraftTextDiffHunk[] | undefined => {
|
||||
const parsed = parseJsonValue(value)
|
||||
return Array.isArray(parsed) ? (parsed as DraftTextDiffHunk[]) : undefined
|
||||
}
|
||||
|
||||
const parseCommitResults = (value: PrimitiveRowValue): CommitResultItem[] => {
|
||||
const parsed = parseJsonValue(value)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed
|
||||
.filter(
|
||||
(item): item is JsonObject =>
|
||||
Boolean(item) && typeof item === 'object' && !Array.isArray(item)
|
||||
)
|
||||
.map((item) => ({
|
||||
review_id: typeof item.review_id === 'string' ? item.review_id : '',
|
||||
output_path: typeof item.output_path === 'string' ? item.output_path : '',
|
||||
operation: item.operation === 'updated' ? 'updated' : 'created',
|
||||
checksum: typeof item.checksum === 'string' ? item.checksum : undefined,
|
||||
byte_size: typeof item.byte_size === 'number' ? item.byte_size : undefined
|
||||
}))
|
||||
.filter((item) => item.review_id && item.output_path)
|
||||
}
|
||||
|
||||
const normalizeFileReviewStatus = (value: PrimitiveRowValue): FileReviewStatus => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
switch (normalized) {
|
||||
case 'pending_confirm':
|
||||
case 'committing':
|
||||
case 'committed':
|
||||
case 'conflicted':
|
||||
case 'discarded':
|
||||
case 'superseded':
|
||||
case 'failed':
|
||||
return normalized
|
||||
default:
|
||||
return 'pending_confirm'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeChangeDecision = (value: PrimitiveRowValue): DraftChangeDecision => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
return normalized === 'accepted' || normalized === 'rejected' ? normalized : 'pending'
|
||||
}
|
||||
|
||||
const normalizeFormat = (value: PrimitiveRowValue): FileReview['format'] => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
switch (normalized) {
|
||||
case 'pdf':
|
||||
case 'xlsx':
|
||||
case 'pptx':
|
||||
case 'md':
|
||||
case 'txt':
|
||||
return normalized
|
||||
default:
|
||||
return 'docx'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeChangeKind = (value: PrimitiveRowValue): FileReviewChange['change_kind'] => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
switch (normalized) {
|
||||
case 'text_hunk':
|
||||
case 'table_cell':
|
||||
case 'worksheet_range':
|
||||
case 'slide_object':
|
||||
case 'page_region':
|
||||
return normalized
|
||||
default:
|
||||
return 'block'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeCommitAttemptStatus = (value: PrimitiveRowValue): CommitAttemptStatus => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
switch (normalized) {
|
||||
case 'running':
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
case 'conflicted':
|
||||
return normalized
|
||||
default:
|
||||
return 'pending'
|
||||
}
|
||||
}
|
||||
|
||||
const toFileReview = (row: FileReviewRow): FileReview => ({
|
||||
id: toRequiredString(row.id),
|
||||
session_id: toRequiredString(row.session_id),
|
||||
run_id: toRequiredString(row.run_id),
|
||||
status: normalizeFileReviewStatus(row.status),
|
||||
title: toRequiredString(row.title),
|
||||
source_document_id: toOptionalString(row.source_document_id),
|
||||
source_path: toRequiredString(row.source_path),
|
||||
source_name: toRequiredString(row.source_name),
|
||||
format: normalizeFormat(row.format),
|
||||
review_render_kind:
|
||||
row.review_render_kind === 'text_hunk' || row.review_render_kind === 'preview_only'
|
||||
? row.review_render_kind
|
||||
: row.review_render_kind === 'sheet_range' || row.review_render_kind === 'slide_element'
|
||||
? row.review_render_kind
|
||||
: 'docx_block',
|
||||
latest_draft_snapshot_id: toRequiredString(row.latest_draft_snapshot_id),
|
||||
draft_artifact_path: toOptionalString(row.draft_artifact_path),
|
||||
working_artifact_path: toOptionalString(row.working_artifact_path),
|
||||
committed_artifact_path: toOptionalString(row.committed_artifact_path),
|
||||
source_version: parseSourceVersion(row.source_version_json),
|
||||
change_count: toRequiredNumber(row.change_count),
|
||||
pending_change_count: toRequiredNumber(row.pending_change_count),
|
||||
accepted_change_count: toRequiredNumber(row.accepted_change_count),
|
||||
rejected_change_count: toRequiredNumber(row.rejected_change_count),
|
||||
created_at: toRequiredNumber(row.created_at),
|
||||
updated_at: toRequiredNumber(row.updated_at),
|
||||
completed_at: toOptionalNumber(row.completed_at),
|
||||
committed_at: toOptionalNumber(row.committed_at),
|
||||
last_error: toOptionalString(row.last_error)
|
||||
})
|
||||
|
||||
const toFileReviewChange = (row: FileReviewChangeRow): FileReviewChange => ({
|
||||
id: toRequiredString(row.id),
|
||||
review_id: toRequiredString(row.review_id),
|
||||
order_index: toRequiredNumber(row.order_index),
|
||||
change_kind: normalizeChangeKind(row.change_kind),
|
||||
title: toRequiredString(row.title),
|
||||
summary: toRequiredString(row.summary),
|
||||
decision: normalizeChangeDecision(row.decision),
|
||||
before_text: toOptionalString(row.before_text),
|
||||
after_text: toOptionalString(row.after_text),
|
||||
anchor_name: toOptionalString(row.anchor_name),
|
||||
locator: parseJsonObject(row.locator_json),
|
||||
diff_hunks: parseDiffHunks(row.diff_hunks_json),
|
||||
source_step_ref: toOptionalString(row.source_step_ref),
|
||||
metadata: parseJsonObject(row.metadata_json),
|
||||
created_at: toRequiredNumber(row.created_at),
|
||||
updated_at: toRequiredNumber(row.updated_at)
|
||||
})
|
||||
|
||||
const toCommitAttempt = (row: CommitAttemptRow): CommitAttempt => ({
|
||||
id: toRequiredString(row.id),
|
||||
review_id: toRequiredString(row.review_id),
|
||||
status: normalizeCommitAttemptStatus(row.status),
|
||||
results: parseCommitResults(row.results_json),
|
||||
started_at: toRequiredNumber(row.started_at),
|
||||
completed_at: toOptionalNumber(row.completed_at),
|
||||
error: toOptionalString(row.error)
|
||||
})
|
||||
|
||||
const omitUndefined = <T extends WriteRow>(value: T): Partial<T> => {
|
||||
const next: Partial<T> = {}
|
||||
for (const key of Object.keys(value) as Array<keyof T>) {
|
||||
if (value[key] !== undefined) next[key] = value[key]
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
const toFileReviewWriteRow = (review: FileReview): WriteRow => ({
|
||||
id: review.id,
|
||||
session_id: review.session_id,
|
||||
run_id: review.run_id,
|
||||
status: review.status,
|
||||
title: review.title,
|
||||
source_document_id: review.source_document_id,
|
||||
source_path: review.source_path,
|
||||
source_name: review.source_name,
|
||||
format: review.format,
|
||||
review_render_kind: review.review_render_kind,
|
||||
latest_draft_snapshot_id: review.latest_draft_snapshot_id,
|
||||
draft_artifact_path: review.draft_artifact_path,
|
||||
working_artifact_path: review.working_artifact_path,
|
||||
committed_artifact_path: review.committed_artifact_path,
|
||||
source_version_json: JSON.stringify(review.source_version),
|
||||
change_count: review.change_count,
|
||||
pending_change_count: review.pending_change_count,
|
||||
accepted_change_count: review.accepted_change_count,
|
||||
rejected_change_count: review.rejected_change_count,
|
||||
created_at: review.created_at,
|
||||
updated_at: review.updated_at,
|
||||
completed_at: review.completed_at,
|
||||
committed_at: review.committed_at,
|
||||
last_error: review.last_error
|
||||
})
|
||||
|
||||
const toFileReviewChangeWriteRow = (change: FileReviewChange): WriteRow => ({
|
||||
id: change.id,
|
||||
review_id: change.review_id,
|
||||
order_index: change.order_index,
|
||||
change_kind: change.change_kind,
|
||||
title: change.title,
|
||||
summary: change.summary,
|
||||
decision: change.decision,
|
||||
before_text: change.before_text,
|
||||
after_text: change.after_text,
|
||||
anchor_name: change.anchor_name,
|
||||
locator_json: change.locator ? JSON.stringify(change.locator) : undefined,
|
||||
diff_hunks_json: change.diff_hunks ? JSON.stringify(change.diff_hunks) : undefined,
|
||||
source_step_ref: change.source_step_ref,
|
||||
metadata_json: change.metadata ? JSON.stringify(change.metadata) : undefined,
|
||||
created_at: change.created_at,
|
||||
updated_at: change.updated_at
|
||||
})
|
||||
|
||||
const toCommitAttemptWriteRow = (attempt: CommitAttempt): WriteRow => ({
|
||||
id: attempt.id,
|
||||
review_id: attempt.review_id,
|
||||
status: attempt.status,
|
||||
results_json: JSON.stringify(attempt.results),
|
||||
started_at: attempt.started_at,
|
||||
completed_at: attempt.completed_at,
|
||||
error: attempt.error
|
||||
})
|
||||
|
||||
export class FileReviewStore {
|
||||
public constructor(private readonly core: LanceDbCore) {}
|
||||
|
||||
public async saveReview(review: FileReview): Promise<void> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.FILE_REVIEW)
|
||||
await table.delete(`id = '${this.core.escapeSqlString(review.id)}'`)
|
||||
await table.add([omitUndefined(toFileReviewWriteRow(review))])
|
||||
}
|
||||
|
||||
public async listReviews(filters?: ListFileReviewsRequest): Promise<ListFileReviewsResponse> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.FILE_REVIEW)
|
||||
const where = this.core.combineWhere([
|
||||
filters?.sessionId ? `session_id = '${this.core.escapeSqlString(filters.sessionId)}'` : undefined,
|
||||
filters?.status ? `status = '${this.core.escapeSqlString(filters.status)}'` : undefined
|
||||
])
|
||||
const rows = (await (where ? table.query().where(where) : table.query()).toArray()) as FileReviewRow[]
|
||||
const items = rows.map(toFileReview).sort((a, b) => b.updated_at - a.updated_at)
|
||||
return { items, total: items.length }
|
||||
}
|
||||
|
||||
public async getReview(reviewId: string): Promise<FileReview | null> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.FILE_REVIEW)
|
||||
const rows = (await table
|
||||
.query()
|
||||
.where(`id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
.limit(1)
|
||||
.toArray()) as FileReviewRow[]
|
||||
return rows[0] ? toFileReview(rows[0]) : null
|
||||
}
|
||||
|
||||
public async replaceChanges(reviewId: string, changes: FileReviewChange[]): Promise<void> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.FILE_REVIEW_CHANGE)
|
||||
await table.delete(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
if (changes.length === 0) return
|
||||
await table.add(changes.map((change) => omitUndefined(toFileReviewChangeWriteRow(change))))
|
||||
}
|
||||
|
||||
public async listChanges(reviewId: string): Promise<FileReviewChange[]> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.FILE_REVIEW_CHANGE)
|
||||
const rows = (await table
|
||||
.query()
|
||||
.where(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
.toArray()) as FileReviewChangeRow[]
|
||||
return rows.map(toFileReviewChange).sort((a, b) => a.order_index - b.order_index)
|
||||
}
|
||||
|
||||
public async replaceCommitAttempts(reviewId: string, attempts: CommitAttempt[]): Promise<void> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.COMMIT_ATTEMPT)
|
||||
await table.delete(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
if (attempts.length === 0) return
|
||||
await table.add(attempts.map((attempt) => omitUndefined(toCommitAttemptWriteRow(attempt))))
|
||||
}
|
||||
|
||||
public async listCommitAttempts(reviewId: string): Promise<CommitAttempt[]> {
|
||||
const table = await this.core.openTable(LANCE_TABLES.COMMIT_ATTEMPT)
|
||||
const rows = (await table
|
||||
.query()
|
||||
.where(`review_id = '${this.core.escapeSqlString(reviewId)}'`)
|
||||
.toArray()) as CommitAttemptRow[]
|
||||
return rows.map(toCommitAttempt).sort((a, b) => a.started_at - b.started_at)
|
||||
}
|
||||
|
||||
public async saveReviewDetail(detail: { review: FileReview; changes: FileReviewChange[]; commit_attempts: CommitAttempt[] }): Promise<void> {
|
||||
await this.saveReview(detail.review)
|
||||
await this.replaceChanges(detail.review.id, detail.changes)
|
||||
await this.replaceCommitAttempts(detail.review.id, detail.commit_attempts)
|
||||
}
|
||||
|
||||
public async getReviewDetail(reviewId: string): Promise<{ review: FileReview; changes: FileReviewChange[]; commit_attempts: CommitAttempt[] } | null> {
|
||||
const review = await this.getReview(reviewId)
|
||||
if (!review) return null
|
||||
const [changes, commitAttempts] = await Promise.all([
|
||||
this.listChanges(reviewId),
|
||||
this.listCommitAttempts(reviewId)
|
||||
])
|
||||
return {
|
||||
review,
|
||||
changes,
|
||||
commit_attempts: commitAttempts
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteReview(reviewId: string): Promise<void> {
|
||||
const safeReviewId = this.core.escapeSqlString(reviewId)
|
||||
const [reviewTable, changeTable, attemptTable] = await Promise.all([
|
||||
this.core.openTable(LANCE_TABLES.FILE_REVIEW),
|
||||
this.core.openTable(LANCE_TABLES.FILE_REVIEW_CHANGE),
|
||||
this.core.openTable(LANCE_TABLES.COMMIT_ATTEMPT)
|
||||
])
|
||||
await reviewTable.delete(`id = '${safeReviewId}'`)
|
||||
await changeTable.delete(`review_id = '${safeReviewId}'`)
|
||||
await attemptTable.delete(`review_id = '${safeReviewId}'`)
|
||||
}
|
||||
|
||||
public async listActiveReviewsBySourcePath(
|
||||
sourcePath: string,
|
||||
statuses: FileReviewStatus[]
|
||||
): Promise<{ review: FileReview; changes: FileReviewChange[]; commit_attempts: CommitAttempt[] }[]> {
|
||||
if (statuses.length === 0) return []
|
||||
const table = await this.core.openTable(LANCE_TABLES.FILE_REVIEW)
|
||||
const where = this.core.combineWhere([
|
||||
`source_path = '${this.core.escapeSqlString(sourcePath)}'`,
|
||||
this.core.buildInClause('status', statuses)
|
||||
])
|
||||
const rows = (await table.query().where(where || '').toArray()) as FileReviewRow[]
|
||||
const reviewIds = rows.map((row) => toRequiredString(row.id)).filter(Boolean)
|
||||
const details = await Promise.all(reviewIds.map((reviewId) => this.getReviewDetail(reviewId)))
|
||||
return details
|
||||
.filter((detail): detail is { review: FileReview; changes: FileReviewChange[]; commit_attempts: CommitAttempt[] } => Boolean(detail))
|
||||
.sort((left, right) => right.review.updated_at - left.review.updated_at)
|
||||
}
|
||||
}
|
||||
Vendored
+44
-42
@@ -45,34 +45,35 @@ import {
|
||||
DraftChangeKind,
|
||||
DraftChangeDecision,
|
||||
DraftChangeResolution,
|
||||
DraftReviewStatus,
|
||||
FileReviewStatus,
|
||||
DraftArtifactRole,
|
||||
DraftCommitJobStatus,
|
||||
DraftCommitValidationIssueCode,
|
||||
CommitAttemptStatus,
|
||||
CommitValidationIssueCode,
|
||||
DraftSourceVersion,
|
||||
DraftReviewSummary,
|
||||
DraftReviewDocument,
|
||||
DraftReviewChange,
|
||||
DraftCommitResultItem,
|
||||
DraftCommitJob,
|
||||
DraftReviewDetail,
|
||||
ListDraftReviewsRequest,
|
||||
ListDraftReviewsResponse,
|
||||
GetDraftReviewRequest,
|
||||
GetDraftReviewResponse,
|
||||
DraftCommitValidationIssue,
|
||||
ValidateDraftCommitRequest,
|
||||
ValidateDraftCommitResponse,
|
||||
CommitDraftReviewRequest,
|
||||
CommitDraftReviewResponse,
|
||||
DiscardDraftReviewRequest,
|
||||
DiscardDraftReviewResponse,
|
||||
FileReview,
|
||||
FileReviewChange,
|
||||
CommitResultItem,
|
||||
CommitAttempt,
|
||||
FileReviewDetail,
|
||||
ListFileReviewsRequest,
|
||||
ListFileReviewsResponse,
|
||||
GetFileReviewRequest,
|
||||
GetFileReviewResponse,
|
||||
CommitValidationIssue,
|
||||
ValidateFileReviewCommitRequest,
|
||||
ValidateFileReviewCommitResponse,
|
||||
CommitFileReviewRequest,
|
||||
CommitFileReviewResponse,
|
||||
DiscardFileReviewRequest,
|
||||
DiscardFileReviewResponse,
|
||||
RuntimeChangedFile,
|
||||
RuntimeArtifactsSummary,
|
||||
PickApplicationResponse,
|
||||
OpenPathWithApplicationRequest,
|
||||
OpenPathWithApplicationResponse,
|
||||
AiRuntimeSettings,
|
||||
AiArtifactRef,
|
||||
AiDraftReviewArtifactRef,
|
||||
AiFileReviewArtifactRef,
|
||||
AiFileArtifactRef,
|
||||
AiRunEvent,
|
||||
AiRunStatus,
|
||||
@@ -133,34 +134,35 @@ export type {
|
||||
DraftChangeKind,
|
||||
DraftChangeDecision,
|
||||
DraftChangeResolution,
|
||||
DraftReviewStatus,
|
||||
FileReviewStatus,
|
||||
DraftArtifactRole,
|
||||
DraftCommitJobStatus,
|
||||
DraftCommitValidationIssueCode,
|
||||
CommitAttemptStatus,
|
||||
CommitValidationIssueCode,
|
||||
DraftSourceVersion,
|
||||
DraftReviewSummary,
|
||||
DraftReviewDocument,
|
||||
DraftReviewChange,
|
||||
DraftCommitResultItem,
|
||||
DraftCommitJob,
|
||||
DraftReviewDetail,
|
||||
ListDraftReviewsRequest,
|
||||
ListDraftReviewsResponse,
|
||||
GetDraftReviewRequest,
|
||||
GetDraftReviewResponse,
|
||||
DraftCommitValidationIssue,
|
||||
ValidateDraftCommitRequest,
|
||||
ValidateDraftCommitResponse,
|
||||
CommitDraftReviewRequest,
|
||||
CommitDraftReviewResponse,
|
||||
DiscardDraftReviewRequest,
|
||||
DiscardDraftReviewResponse,
|
||||
FileReview,
|
||||
FileReviewChange,
|
||||
CommitResultItem,
|
||||
CommitAttempt,
|
||||
FileReviewDetail,
|
||||
ListFileReviewsRequest,
|
||||
ListFileReviewsResponse,
|
||||
GetFileReviewRequest,
|
||||
GetFileReviewResponse,
|
||||
CommitValidationIssue,
|
||||
ValidateFileReviewCommitRequest,
|
||||
ValidateFileReviewCommitResponse,
|
||||
CommitFileReviewRequest,
|
||||
CommitFileReviewResponse,
|
||||
DiscardFileReviewRequest,
|
||||
DiscardFileReviewResponse,
|
||||
RuntimeChangedFile,
|
||||
RuntimeArtifactsSummary,
|
||||
PickApplicationResponse,
|
||||
OpenPathWithApplicationRequest,
|
||||
OpenPathWithApplicationResponse,
|
||||
AiRuntimeSettings,
|
||||
AiArtifactRef,
|
||||
AiDraftReviewArtifactRef,
|
||||
AiFileReviewArtifactRef,
|
||||
AiFileArtifactRef,
|
||||
AiRunEvent,
|
||||
AiRunStatus,
|
||||
|
||||
+17
-17
@@ -8,19 +8,19 @@ import type {
|
||||
AiRunStartResponse,
|
||||
ChatMessage,
|
||||
ChatSession,
|
||||
CommitDraftReviewRequest,
|
||||
CommitDraftReviewResponse,
|
||||
CommitFileReviewRequest,
|
||||
CommitFileReviewResponse,
|
||||
ChunkListResponse,
|
||||
CreateWorkspaceSourceRequest,
|
||||
DeleteWebSourceRequest,
|
||||
DeleteWebSourceResponse,
|
||||
DeleteWorkspaceSourceResponse,
|
||||
DocumentRecord,
|
||||
GetDraftReviewRequest,
|
||||
GetDraftReviewResponse,
|
||||
GetFileReviewRequest,
|
||||
GetFileReviewResponse,
|
||||
LegacyLocalImportMigrationStatus,
|
||||
ListDraftReviewsRequest,
|
||||
ListDraftReviewsResponse,
|
||||
ListFileReviewsRequest,
|
||||
ListFileReviewsResponse,
|
||||
ListReferenceItemsRequest,
|
||||
ListWebSourcesRequest,
|
||||
ListWorkspaceNodesRequest,
|
||||
@@ -31,14 +31,14 @@ import type {
|
||||
SearchResponse,
|
||||
SearchResult,
|
||||
SearchSourceFilter,
|
||||
DiscardDraftReviewRequest,
|
||||
DiscardDraftReviewResponse,
|
||||
DiscardFileReviewRequest,
|
||||
DiscardFileReviewResponse,
|
||||
PickApplicationResponse,
|
||||
OpenPathWithApplicationRequest,
|
||||
OpenPathWithApplicationResponse,
|
||||
ReferenceItemListResponse,
|
||||
ValidateDraftCommitRequest,
|
||||
ValidateDraftCommitResponse,
|
||||
ValidateFileReviewCommitRequest,
|
||||
ValidateFileReviewCommitResponse,
|
||||
WebSourceListResponse,
|
||||
WebSourceMutationResponse,
|
||||
WebIngestPayload,
|
||||
@@ -161,13 +161,13 @@ export type DesktopApi = {
|
||||
aiRunApprovalDecide: (
|
||||
payload: AiRunApprovalDecisionRequest
|
||||
) => Promise<AiRunApprovalDecisionResponse>
|
||||
draftReviewList: (payload?: ListDraftReviewsRequest) => Promise<ListDraftReviewsResponse>
|
||||
draftReviewGet: (payload: GetDraftReviewRequest) => Promise<GetDraftReviewResponse>
|
||||
draftReviewValidate: (payload: ValidateDraftCommitRequest) => Promise<ValidateDraftCommitResponse>
|
||||
draftReviewCommit: (payload: CommitDraftReviewRequest) => Promise<CommitDraftReviewResponse>
|
||||
draftReviewDiscard: (
|
||||
payload: DiscardDraftReviewRequest
|
||||
) => Promise<DiscardDraftReviewResponse>
|
||||
fileReviewList: (payload?: ListFileReviewsRequest) => Promise<ListFileReviewsResponse>
|
||||
fileReviewGet: (payload: GetFileReviewRequest) => Promise<GetFileReviewResponse>
|
||||
fileReviewValidate: (payload: ValidateFileReviewCommitRequest) => Promise<ValidateFileReviewCommitResponse>
|
||||
fileReviewCommit: (payload: CommitFileReviewRequest) => Promise<CommitFileReviewResponse>
|
||||
fileReviewDiscard: (
|
||||
payload: DiscardFileReviewRequest
|
||||
) => Promise<DiscardFileReviewResponse>
|
||||
pickApplication: () => Promise<PickApplicationResponse>
|
||||
openPathWithApplication: (
|
||||
payload: OpenPathWithApplicationRequest
|
||||
|
||||
+11
-11
@@ -173,19 +173,19 @@ const api: DesktopApi = {
|
||||
emitE2EIpcCall('aiRunApprovalDecide')
|
||||
return ipcRenderer.invoke('ai-run-approval-decide', payload)
|
||||
},
|
||||
draftReviewList: (payload) => ipcRenderer.invoke('draft-review-list', payload),
|
||||
draftReviewGet: (payload) => ipcRenderer.invoke('draft-review-get', payload),
|
||||
draftReviewValidate: (payload) => {
|
||||
emitE2EIpcCall('draftReviewValidate')
|
||||
return ipcRenderer.invoke('draft-review-validate', payload)
|
||||
fileReviewList: (payload) => ipcRenderer.invoke('file-review-list', payload),
|
||||
fileReviewGet: (payload) => ipcRenderer.invoke('file-review-get', payload),
|
||||
fileReviewValidate: (payload) => {
|
||||
emitE2EIpcCall('fileReviewValidate')
|
||||
return ipcRenderer.invoke('file-review-validate', payload)
|
||||
},
|
||||
draftReviewCommit: (payload) => {
|
||||
emitE2EIpcCall('draftReviewCommit')
|
||||
return ipcRenderer.invoke('draft-review-commit', payload)
|
||||
fileReviewCommit: (payload) => {
|
||||
emitE2EIpcCall('fileReviewCommit')
|
||||
return ipcRenderer.invoke('file-review-commit', payload)
|
||||
},
|
||||
draftReviewDiscard: (payload) => {
|
||||
emitE2EIpcCall('draftReviewDiscard')
|
||||
return ipcRenderer.invoke('draft-review-discard', payload)
|
||||
fileReviewDiscard: (payload) => {
|
||||
emitE2EIpcCall('fileReviewDiscard')
|
||||
return ipcRenderer.invoke('file-review-discard', payload)
|
||||
},
|
||||
onAiRunEvent: (cb: (event: AiRunEvent) => void) =>
|
||||
ipcRenderer.on('ai-run-event', (_event, data) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Empty, Modal } from 'antd'
|
||||
import { HiOutlineMagnifyingGlass } from 'react-icons/hi2'
|
||||
import type { DraftReviewDetail, ValidateDraftCommitResponse } from '../../../../shared/types'
|
||||
import type { FileReviewDetail, ValidateFileReviewCommitResponse } from '../../../../shared/types'
|
||||
import EmbeddedFileViewer from '../fileRenders/EmbeddedFileViewer'
|
||||
import WorkbenchReviewPanel from './WorkbenchReviewPanel'
|
||||
import type {
|
||||
@@ -13,9 +13,9 @@ import { workbenchSelectors } from '../../testing/selectors'
|
||||
|
||||
type WorkbenchContentPanelProps = {
|
||||
state: WorkbenchContentPanelState
|
||||
activeReview: DraftReviewDetail | null
|
||||
draftReviewLoading: boolean
|
||||
draftCommitValidation: ValidateDraftCommitResponse | null
|
||||
activeReview: FileReviewDetail | null
|
||||
fileReviewLoading: boolean
|
||||
draftCommitValidation: ValidateFileReviewCommitResponse | null
|
||||
draftCommitSubmitting: boolean
|
||||
draftDiscardSubmitting: boolean
|
||||
modalPreviewFile: WorkbenchFileRef | null
|
||||
@@ -32,7 +32,7 @@ type WorkbenchContentPanelProps = {
|
||||
const WorkbenchContentPanel: React.FC<WorkbenchContentPanelProps> = ({
|
||||
state,
|
||||
activeReview,
|
||||
draftReviewLoading,
|
||||
fileReviewLoading,
|
||||
draftCommitValidation,
|
||||
draftCommitSubmitting,
|
||||
draftDiscardSubmitting,
|
||||
@@ -190,7 +190,7 @@ const WorkbenchContentPanel: React.FC<WorkbenchContentPanelProps> = ({
|
||||
{state.kind === 'review_workspace' ? (
|
||||
<WorkbenchReviewPanel
|
||||
review={activeReview}
|
||||
loading={draftReviewLoading}
|
||||
loading={fileReviewLoading}
|
||||
validation={draftCommitValidation}
|
||||
committing={draftCommitSubmitting}
|
||||
discarding={draftDiscardSubmitting}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Spin } from 'antd'
|
||||
import { HiChevronLeft, HiChevronRight } from 'react-icons/hi2'
|
||||
import type { DraftReviewDetail, ValidateDraftCommitResponse } from '../../../../shared/types'
|
||||
import type { FileReviewDetail, ValidateFileReviewCommitResponse } from '../../../../shared/types'
|
||||
import EmbeddedFileViewer from '../fileRenders/EmbeddedFileViewer'
|
||||
import { REVIEW_STATUS_META } from './constants'
|
||||
import type { ReviewWorkspaceTab } from './types'
|
||||
@@ -9,7 +9,7 @@ import { workbenchSelectors } from '../../testing/selectors'
|
||||
|
||||
const ACTIVE_CONFIRM_STATUSES = new Set(['pending_confirm', 'conflicted', 'failed'])
|
||||
|
||||
const getEndedStateMessage = (status: DraftReviewDetail['review']['status']): string => {
|
||||
const getEndedStateMessage = (status: FileReviewDetail['review']['status']): string => {
|
||||
if (status === 'committed') {
|
||||
return '本文件修改已写回源文件。'
|
||||
}
|
||||
@@ -23,9 +23,9 @@ const getEndedStateMessage = (status: DraftReviewDetail['review']['status']): st
|
||||
}
|
||||
|
||||
type WorkbenchReviewPanelProps = {
|
||||
review: DraftReviewDetail | null
|
||||
review: FileReviewDetail | null
|
||||
loading: boolean
|
||||
validation: ValidateDraftCommitResponse | null
|
||||
validation: ValidateFileReviewCommitResponse | null
|
||||
committing: boolean
|
||||
discarding: boolean
|
||||
tabs: ReviewWorkspaceTab[]
|
||||
@@ -47,13 +47,10 @@ const WorkbenchReviewPanel: React.FC<WorkbenchReviewPanelProps> = ({
|
||||
}) => {
|
||||
const [selectedChangeIndex, setSelectedChangeIndex] = useState(0)
|
||||
|
||||
const selectedDocument = review?.documents[0] || null
|
||||
const documentChanges = useMemo(() => {
|
||||
if (!review || !selectedDocument) return []
|
||||
return review.changes
|
||||
.filter((item) => item.document_id === selectedDocument.id)
|
||||
.sort((left, right) => left.order_index - right.order_index)
|
||||
}, [review, selectedDocument])
|
||||
if (!review) return []
|
||||
return [...review.changes].sort((left, right) => left.order_index - right.order_index)
|
||||
}, [review])
|
||||
|
||||
useEffect(() => {
|
||||
if (documentChanges.length === 0) {
|
||||
@@ -66,18 +63,16 @@ const WorkbenchReviewPanel: React.FC<WorkbenchReviewPanelProps> = ({
|
||||
const selectedChange = documentChanges[selectedChangeIndex] || null
|
||||
const reviewStatusMeta = review ? REVIEW_STATUS_META[review.review.status] : null
|
||||
const isActiveConfirmSession = Boolean(
|
||||
review &&
|
||||
selectedDocument?.review_artifact_path &&
|
||||
ACTIVE_CONFIRM_STATUSES.has(review.review.status)
|
||||
review && review.review.draft_artifact_path && ACTIVE_CONFIRM_STATUSES.has(review.review.status)
|
||||
)
|
||||
const reviewArtifactPath = selectedDocument?.review_artifact_path
|
||||
const reviewFileName = selectedDocument
|
||||
? `${selectedDocument.source_name.replace(/\.docx$/i, '')}-审阅稿.docx`
|
||||
const reviewArtifactPath = review?.review.draft_artifact_path
|
||||
const reviewFileName = review
|
||||
? `${review.review.source_name.replace(/\.docx$/i, '')}-审阅稿.docx`
|
||||
: undefined
|
||||
const viewerPath =
|
||||
isActiveConfirmSession && reviewArtifactPath ? reviewArtifactPath : selectedDocument?.source_path
|
||||
isActiveConfirmSession && reviewArtifactPath ? reviewArtifactPath : review?.review.source_path
|
||||
const viewerName =
|
||||
isActiveConfirmSession && reviewFileName ? reviewFileName : selectedDocument?.source_name
|
||||
isActiveConfirmSession && reviewFileName ? reviewFileName : review?.review.source_name
|
||||
const endedStateMessage = review ? getEndedStateMessage(review.review.status) : ''
|
||||
const commitDisabled =
|
||||
!review ||
|
||||
@@ -99,7 +94,7 @@ const WorkbenchReviewPanel: React.FC<WorkbenchReviewPanelProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (!review || !selectedDocument) {
|
||||
if (!review) {
|
||||
return (
|
||||
<div
|
||||
className="ui-inset flex h-full items-center justify-center border-dashed text-sm text-[#999999]"
|
||||
@@ -118,7 +113,7 @@ const WorkbenchReviewPanel: React.FC<WorkbenchReviewPanelProps> = ({
|
||||
data-testid={workbenchSelectors.reviewTabs}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const active = tab.fileKey === selectedDocument.source_path
|
||||
const active = tab.fileKey === review.review.source_path
|
||||
return (
|
||||
<button
|
||||
key={tab.fileKey}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AssistantStepStatus, WorkbenchFileOrigin } from './types'
|
||||
import type { DraftReviewStatus } from '../../../../shared/types'
|
||||
import type { FileReviewStatus } from '../../../../shared/types'
|
||||
|
||||
export const WORKBENCH_SIDEBAR_STORAGE_KEY = 'workbench.sidebar.collapsed'
|
||||
export const WORKBENCH_CONTENT_PANEL_COLLAPSED_STORAGE_KEY = 'workbench.content-panel.collapsed'
|
||||
@@ -14,7 +14,7 @@ export const ASSISTANT_STEP_STATUS_META: Record<
|
||||
failed: { label: '失败', className: 'border-[#F7D5D9] bg-[#FFF2F4] text-[#B5455D]' }
|
||||
}
|
||||
|
||||
export const REVIEW_STATUS_META: Record<DraftReviewStatus, { label: string; className: string }> = {
|
||||
export const REVIEW_STATUS_META: Record<FileReviewStatus, { label: string; className: string }> = {
|
||||
pending_confirm: {
|
||||
label: '待确认修改',
|
||||
className: 'border-[#F4E5DF] bg-[#FBF5F2] text-[#D97757]'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { DraftReviewSummary } from '../../../../shared/types'
|
||||
import type { FileReview } from '../../../../shared/types'
|
||||
import type { AssistantTurnReviewTarget } from './types'
|
||||
|
||||
export const buildReviewTarget = (review: DraftReviewSummary): AssistantTurnReviewTarget => ({
|
||||
export const buildReviewTarget = (review: FileReview): AssistantTurnReviewTarget => ({
|
||||
reviewId: review.id,
|
||||
title: review.title,
|
||||
changeCount: review.change_count,
|
||||
documentCount: review.document_count,
|
||||
documentCount: 1,
|
||||
status: review.status
|
||||
})
|
||||
|
||||
@@ -18,9 +18,9 @@ export const mergeReviewTargets = (
|
||||
]
|
||||
|
||||
export const groupReviewTargetsByRunId = (
|
||||
reviews: DraftReviewSummary[]
|
||||
reviews: FileReview[]
|
||||
): Map<string, AssistantTurnReviewTarget[]> => {
|
||||
const grouped = new Map<string, DraftReviewSummary[]>()
|
||||
const grouped = new Map<string, FileReview[]>()
|
||||
reviews.forEach((review) => {
|
||||
const existing = grouped.get(review.run_id) || []
|
||||
existing.push(review)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
DraftDocumentFormat,
|
||||
DraftReviewStatus,
|
||||
FileReviewStatus,
|
||||
SearchResult
|
||||
} from '../../../../shared/types'
|
||||
|
||||
@@ -38,7 +38,7 @@ export type ReviewWorkspaceTab = {
|
||||
fileName: string
|
||||
sourcePath: string
|
||||
changeCount: number
|
||||
status: DraftReviewStatus
|
||||
status: FileReviewStatus
|
||||
unseen: boolean
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export type AssistantTurnReviewTarget = {
|
||||
title: string
|
||||
changeCount: number
|
||||
documentCount: number
|
||||
status?: DraftReviewStatus
|
||||
status?: FileReviewStatus
|
||||
}
|
||||
|
||||
export type AssistantTurnBlocker = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
DraftReviewChange,
|
||||
DraftReviewDocument,
|
||||
DraftReviewRenderKind,
|
||||
FileReviewChange,
|
||||
FileReview,
|
||||
FileReviewRenderKind,
|
||||
DraftTextDiffHunk,
|
||||
IpcJsonArray,
|
||||
IpcJsonObject,
|
||||
@@ -176,7 +176,7 @@ export const getStepFiles = (step: AssistantTurnStep): WorkbenchFileRef[] => {
|
||||
return dedupeFileRefs(step.files)
|
||||
}
|
||||
|
||||
export const getReviewRenderKind = (document: DraftReviewDocument): DraftReviewRenderKind => {
|
||||
export const getReviewRenderKind = (document: FileReview): FileReviewRenderKind => {
|
||||
if (document.review_render_kind) return document.review_render_kind
|
||||
if (document.format === 'docx') return 'docx_block'
|
||||
if (document.format === 'md' || document.format === 'txt') return 'text_hunk'
|
||||
@@ -185,20 +185,22 @@ export const getReviewRenderKind = (document: DraftReviewDocument): DraftReviewR
|
||||
return 'preview_only'
|
||||
}
|
||||
|
||||
export const formatLocatorSummary = (change: DraftReviewChange): string => {
|
||||
const section = typeof change.locator.section === 'string' ? change.locator.section : null
|
||||
const heading = typeof change.locator.heading === 'string' ? change.locator.heading : null
|
||||
const blockId = typeof change.locator.blockId === 'string' ? change.locator.blockId : null
|
||||
const action = typeof change.locator.action === 'string' ? change.locator.action : null
|
||||
export const formatLocatorSummary = (change: FileReviewChange): string => {
|
||||
const locator = change.locator
|
||||
if (!locator) return '定位信息未结构化'
|
||||
const section = typeof locator.section === 'string' ? locator.section : null
|
||||
const heading = typeof locator.heading === 'string' ? locator.heading : null
|
||||
const blockId = typeof locator.blockId === 'string' ? locator.blockId : null
|
||||
const action = typeof locator.action === 'string' ? locator.action : null
|
||||
const paragraphIndex =
|
||||
typeof change.locator.paragraphIndex === 'number'
|
||||
? `第 ${change.locator.paragraphIndex + 1} 段`
|
||||
typeof locator.paragraphIndex === 'number'
|
||||
? `第 ${locator.paragraphIndex + 1} 段`
|
||||
: null
|
||||
const rowIndex =
|
||||
typeof change.locator.rowIndex === 'number' ? `第 ${change.locator.rowIndex + 1} 行` : null
|
||||
typeof locator.rowIndex === 'number' ? `第 ${locator.rowIndex + 1} 行` : null
|
||||
const columnIndex =
|
||||
typeof change.locator.columnIndex === 'number'
|
||||
? `第 ${change.locator.columnIndex + 1} 列`
|
||||
typeof locator.columnIndex === 'number'
|
||||
? `第 ${locator.columnIndex + 1} 列`
|
||||
: null
|
||||
|
||||
return (
|
||||
@@ -209,10 +211,10 @@ export const formatLocatorSummary = (change: DraftReviewChange): string => {
|
||||
)
|
||||
}
|
||||
|
||||
export const buildFallbackDiffHunks = (change: DraftReviewChange): DraftTextDiffHunk[] => {
|
||||
export const buildFallbackDiffHunks = (change: FileReviewChange): DraftTextDiffHunk[] => {
|
||||
if (change.diff_hunks && change.diff_hunks.length > 0) return change.diff_hunks
|
||||
const beforeLines = change.before_text.split('\n')
|
||||
const afterLines = change.after_text.split('\n')
|
||||
const beforeLines = String(change.before_text || '').split('\n')
|
||||
const afterLines = String(change.after_text || '').split('\n')
|
||||
return [
|
||||
{
|
||||
id: change.id,
|
||||
@@ -226,7 +228,7 @@ export const buildFallbackDiffHunks = (change: DraftReviewChange): DraftTextDiff
|
||||
]
|
||||
}
|
||||
|
||||
export const buildUnifiedDiffText = (change: DraftReviewChange): string => {
|
||||
export const buildUnifiedDiffText = (change: FileReviewChange): string => {
|
||||
return buildFallbackDiffHunks(change)
|
||||
.flatMap((hunk) =>
|
||||
hunk.lines.map(
|
||||
|
||||
@@ -15,12 +15,12 @@ import type {
|
||||
AiRunEvent,
|
||||
ChatMessage,
|
||||
ChatSession,
|
||||
DraftReviewDetail,
|
||||
DraftReviewSummary,
|
||||
FileReviewDetail,
|
||||
FileReview,
|
||||
LLMConfig,
|
||||
SearchResult,
|
||||
SelectedReference,
|
||||
ValidateDraftCommitResponse,
|
||||
ValidateFileReviewCommitResponse,
|
||||
WorkspaceSource,
|
||||
WorkspaceRuntimeBinding
|
||||
} from '../../../shared/types'
|
||||
@@ -84,14 +84,14 @@ const createAssistantTurn = (messageId: string): AssistantTurnViewModel => ({
|
||||
blockers: []
|
||||
})
|
||||
|
||||
const ACTIVE_REVIEW_WORKSPACE_STATUSES = new Set<DraftReviewSummary['status']>([
|
||||
const ACTIVE_REVIEW_WORKSPACE_STATUSES = new Set<FileReview['status']>([
|
||||
'pending_confirm',
|
||||
'conflicted',
|
||||
'failed'
|
||||
])
|
||||
|
||||
const sortDraftReviews = (items: DraftReviewSummary[]): DraftReviewSummary[] => {
|
||||
const priority: Record<DraftReviewSummary['status'], number> = {
|
||||
const sortFileReviews = (items: FileReview[]): FileReview[] => {
|
||||
const priority: Record<FileReview['status'], number> = {
|
||||
pending_confirm: 0,
|
||||
conflicted: 1,
|
||||
failed: 2,
|
||||
@@ -153,10 +153,10 @@ const ChatPage: React.FC = () => {
|
||||
return stored === null ? true : stored === 'true'
|
||||
})
|
||||
|
||||
const [activeDraftReview, setActiveDraftReview] = useState<DraftReviewDetail | null>(null)
|
||||
const [draftReviewLoading, setDraftReviewLoading] = useState(false)
|
||||
const [activeFileReview, setActiveFileReview] = useState<FileReviewDetail | null>(null)
|
||||
const [fileReviewLoading, setFileReviewLoading] = useState(false)
|
||||
const [draftCommitValidation, setDraftCommitValidation] =
|
||||
useState<ValidateDraftCommitResponse | null>(null)
|
||||
useState<ValidateFileReviewCommitResponse | null>(null)
|
||||
const [draftCommitSubmitting, setDraftCommitSubmitting] = useState(false)
|
||||
const [draftDiscardSubmitting, setDraftDiscardSubmitting] = useState(false)
|
||||
|
||||
@@ -270,10 +270,10 @@ const ChatPage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (contentPanel.kind === 'review_workspace' && activeDraftReview) {
|
||||
const statusMeta = REVIEW_STATUS_META[activeDraftReview.review.status]
|
||||
if (contentPanel.kind === 'review_workspace' && activeFileReview) {
|
||||
const statusMeta = REVIEW_STATUS_META[activeFileReview.review.status]
|
||||
return {
|
||||
title: activeDraftReview.documents[0]?.source_name || contentPanel.title,
|
||||
title: activeFileReview.review.source_name || contentPanel.title,
|
||||
badge: statusMeta.label,
|
||||
badgeClassName: statusMeta.className
|
||||
}
|
||||
@@ -286,9 +286,9 @@ const ChatPage: React.FC = () => {
|
||||
}
|
||||
}, [contentPanel, contentPanelCollapsed])
|
||||
|
||||
const resetDraftReviewState = (): void => {
|
||||
setActiveDraftReview(null)
|
||||
setDraftReviewLoading(false)
|
||||
const resetFileReviewState = (): void => {
|
||||
setActiveFileReview(null)
|
||||
setFileReviewLoading(false)
|
||||
setDraftCommitValidation(null)
|
||||
setDraftCommitSubmitting(false)
|
||||
setDraftDiscardSubmitting(false)
|
||||
@@ -386,42 +386,42 @@ const ChatPage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadDraftReviewDetail = async (input: {
|
||||
const loadFileReviewDetail = async (input: {
|
||||
reviewId: string
|
||||
detail?: DraftReviewDetail | null
|
||||
detail?: FileReviewDetail | null
|
||||
tabs?: ReviewWorkspaceTab[]
|
||||
activeFileKey?: string | null
|
||||
openWorkspace?: boolean
|
||||
}): Promise<void> => {
|
||||
setDraftReviewLoading(true)
|
||||
setFileReviewLoading(true)
|
||||
try {
|
||||
const detailResponse =
|
||||
input.detail !== undefined ? { review: input.detail } : await window.api.draftReviewGet({ reviewId: input.reviewId })
|
||||
input.detail !== undefined ? { review: input.detail } : await window.api.fileReviewGet({ reviewId: input.reviewId })
|
||||
if (!detailResponse.review) {
|
||||
setActiveDraftReview(null)
|
||||
setActiveFileReview(null)
|
||||
setDraftCommitValidation(null)
|
||||
return
|
||||
}
|
||||
|
||||
setActiveDraftReview(detailResponse.review)
|
||||
setActiveFileReview(detailResponse.review)
|
||||
setDraftCommitValidation(null)
|
||||
|
||||
if (input.openWorkspace) {
|
||||
showReviewWorkspace(
|
||||
input.tabs || reviewWorkspaceTabs,
|
||||
input.activeFileKey || detailResponse.review.documents[0]?.source_path || null
|
||||
input.activeFileKey || detailResponse.review.review.source_path || null
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load draft review detail', error)
|
||||
setActiveDraftReview(null)
|
||||
setActiveFileReview(null)
|
||||
setDraftCommitValidation(null)
|
||||
} finally {
|
||||
setDraftReviewLoading(false)
|
||||
setFileReviewLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadDraftReviewsForSession = async (
|
||||
const loadFileReviewsForSession = async (
|
||||
sessionId: string,
|
||||
options?: {
|
||||
preferredReviewId?: string | null
|
||||
@@ -433,13 +433,13 @@ const ChatPage: React.FC = () => {
|
||||
const shouldWaitForCompleteDetails =
|
||||
Boolean(options?.forceOpenWorkspace) || Boolean(options?.preferredReviewId)
|
||||
const maxAttempts = shouldWaitForCompleteDetails ? 6 : 1
|
||||
let nextItems: DraftReviewSummary[] = []
|
||||
let activeItems: DraftReviewSummary[] = []
|
||||
let activeDetails: DraftReviewDetail[] = []
|
||||
let nextItems: FileReview[] = []
|
||||
let activeItems: FileReview[] = []
|
||||
let activeDetails: FileReviewDetail[] = []
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
const response = await window.api.draftReviewList({ sessionId })
|
||||
nextItems = sortDraftReviews(response.items)
|
||||
const response = await window.api.fileReviewList({ sessionId })
|
||||
nextItems = sortFileReviews(response.items)
|
||||
activeItems = nextItems.filter((item) =>
|
||||
ACTIVE_REVIEW_WORKSPACE_STATUSES.has(item.status)
|
||||
)
|
||||
@@ -450,11 +450,11 @@ const ChatPage: React.FC = () => {
|
||||
}
|
||||
|
||||
const activeDetailsResponses = await Promise.all(
|
||||
activeItems.map((item) => window.api.draftReviewGet({ reviewId: item.id }))
|
||||
activeItems.map((item) => window.api.fileReviewGet({ reviewId: item.id }))
|
||||
)
|
||||
activeDetails = activeDetailsResponses
|
||||
.map((item) => item.review)
|
||||
.filter((item): item is DraftReviewDetail => Boolean(item))
|
||||
.filter((item): item is FileReviewDetail => Boolean(item))
|
||||
.sort((left, right) => right.review.updated_at - left.review.updated_at)
|
||||
|
||||
const missingPreferredReview =
|
||||
@@ -488,7 +488,7 @@ const ChatPage: React.FC = () => {
|
||||
|
||||
if (activeItems.length === 0) {
|
||||
setReviewWorkspaceTabs([])
|
||||
setActiveDraftReview(null)
|
||||
setActiveFileReview(null)
|
||||
setDraftCommitValidation(null)
|
||||
setContentPanel((current) =>
|
||||
current.kind === 'review_workspace' ? createEmptyPanel() : current
|
||||
@@ -496,9 +496,9 @@ const ChatPage: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const latestByFileKey = new Map<string, DraftReviewDetail>()
|
||||
const latestByFileKey = new Map<string, FileReviewDetail>()
|
||||
activeDetails.forEach((detail) => {
|
||||
const fileKey = detail.documents[0]?.source_path
|
||||
const fileKey = detail.review.source_path
|
||||
if (!fileKey || latestByFileKey.has(fileKey)) return
|
||||
latestByFileKey.set(fileKey, detail)
|
||||
})
|
||||
@@ -506,17 +506,17 @@ const ChatPage: React.FC = () => {
|
||||
const currentWorkspaceFileKey =
|
||||
contentPanel.kind === 'review_workspace'
|
||||
? contentPanel.activeFileKey
|
||||
: activeDraftReview?.documents[0]?.source_path || null
|
||||
: activeFileReview?.review.source_path || null
|
||||
const previewFileKey =
|
||||
contentPanel.kind === 'file_preview' ? contentPanel.file.path : null
|
||||
const preferredFileKey =
|
||||
options?.preferredReviewId
|
||||
? [...latestByFileKey.values()].find(
|
||||
(detail) => detail.review.id === options.preferredReviewId
|
||||
)?.documents[0]?.source_path || null
|
||||
)?.review.source_path || null
|
||||
: null
|
||||
const nextTabs = [...latestByFileKey.values()].map((detail) => {
|
||||
const fileKey = detail.documents[0]?.source_path || detail.review.id
|
||||
const fileKey = detail.review.source_path || detail.review.id
|
||||
const previousTab = reviewWorkspaceTabs.find((tab) => tab.fileKey === fileKey)
|
||||
const shouldMarkUnseen =
|
||||
previousTab?.reviewId !== detail.review.id &&
|
||||
@@ -527,7 +527,7 @@ const ChatPage: React.FC = () => {
|
||||
return {
|
||||
fileKey,
|
||||
reviewId: detail.review.id,
|
||||
fileName: detail.documents[0]?.source_name || detail.review.title,
|
||||
fileName: detail.review.source_name || detail.review.title,
|
||||
sourcePath: fileKey,
|
||||
changeCount: detail.review.pending_change_count,
|
||||
status: detail.review.status,
|
||||
@@ -577,7 +577,7 @@ const ChatPage: React.FC = () => {
|
||||
(contentPanel.kind === 'empty' ||
|
||||
(previewFileKey !== null && previewFileKey === nextSelectedTab.fileKey)))
|
||||
|
||||
await loadDraftReviewDetail({
|
||||
await loadFileReviewDetail({
|
||||
reviewId: nextSelectedTab.reviewId,
|
||||
detail: nextSelectedDetail,
|
||||
tabs: normalizedTabs,
|
||||
@@ -586,7 +586,7 @@ const ChatPage: React.FC = () => {
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to load draft reviews', error)
|
||||
setActiveDraftReview(null)
|
||||
setActiveFileReview(null)
|
||||
setDraftCommitValidation(null)
|
||||
}
|
||||
}
|
||||
@@ -643,13 +643,13 @@ const ChatPage: React.FC = () => {
|
||||
setPendingWorkspaceRequest(null)
|
||||
setAssistantTurns({})
|
||||
setContentPanel(createEmptyPanel())
|
||||
resetDraftReviewState()
|
||||
void loadDraftReviewsForSession(currentSessionId, { autoOpen: true })
|
||||
resetFileReviewState()
|
||||
void loadFileReviewsForSession(currentSessionId, { autoOpen: true })
|
||||
} else {
|
||||
setCurrentMessages([])
|
||||
setAssistantTurns({})
|
||||
setModalPreviewFile(null)
|
||||
resetDraftReviewState()
|
||||
resetFileReviewState()
|
||||
}
|
||||
}, [currentSessionId])
|
||||
|
||||
@@ -722,9 +722,9 @@ const ChatPage: React.FC = () => {
|
||||
setModalPreviewFile(fileRef)
|
||||
}
|
||||
|
||||
const openDraftReview = (reviewId: string): void => {
|
||||
const openFileReview = (reviewId: string): void => {
|
||||
if (currentSessionId) {
|
||||
void loadDraftReviewsForSession(currentSessionId, {
|
||||
void loadFileReviewsForSession(currentSessionId, {
|
||||
preferredReviewId: reviewId,
|
||||
forceOpenWorkspace: true
|
||||
})
|
||||
@@ -738,7 +738,7 @@ const ChatPage: React.FC = () => {
|
||||
)
|
||||
: reviewWorkspaceTabs
|
||||
setReviewWorkspaceTabs(nextTabs)
|
||||
void loadDraftReviewDetail({
|
||||
void loadFileReviewDetail({
|
||||
reviewId,
|
||||
tabs: nextTabs,
|
||||
activeFileKey: nextTab?.fileKey || null,
|
||||
@@ -925,7 +925,7 @@ const ChatPage: React.FC = () => {
|
||||
message.success(`审阅稿已生成:${event.review.changeCount} 处修改`)
|
||||
const shouldAutoOpen =
|
||||
contentPanel.kind === 'review_workspace' || contentPanel.kind === 'file_preview'
|
||||
void loadDraftReviewsForSession(currentSessionId, {
|
||||
void loadFileReviewsForSession(currentSessionId, {
|
||||
preferredReviewId: event.review.reviewId,
|
||||
autoOpen: shouldAutoOpen
|
||||
})
|
||||
@@ -1342,19 +1342,19 @@ const ChatPage: React.FC = () => {
|
||||
}
|
||||
|
||||
const handleDraftCommit = async (): Promise<void> => {
|
||||
if (!activeDraftReview || !currentSessionId || draftCommitSubmitting) return
|
||||
if (!activeFileReview || !currentSessionId || draftCommitSubmitting) return
|
||||
setDraftCommitSubmitting(true)
|
||||
try {
|
||||
const validation = await window.api.draftReviewValidate({
|
||||
reviewId: activeDraftReview.review.id
|
||||
const validation = await window.api.fileReviewValidate({
|
||||
reviewId: activeFileReview.review.id
|
||||
})
|
||||
setDraftCommitValidation(validation)
|
||||
if (!validation.ready) {
|
||||
message.warning('提交前校验未通过,请先处理提示问题。')
|
||||
return
|
||||
}
|
||||
const response = await window.api.draftReviewCommit({
|
||||
reviewId: activeDraftReview.review.id,
|
||||
const response = await window.api.fileReviewCommit({
|
||||
reviewId: activeFileReview.review.id,
|
||||
overwrite: true
|
||||
})
|
||||
if (!response.success) {
|
||||
@@ -1362,7 +1362,7 @@ const ChatPage: React.FC = () => {
|
||||
} else {
|
||||
emitSuccessMessage('审阅稿已写回源文件')
|
||||
}
|
||||
await loadDraftReviewsForSession(currentSessionId, { autoOpen: true })
|
||||
await loadFileReviewsForSession(currentSessionId, { autoOpen: true })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('提交草稿失败')
|
||||
@@ -1372,11 +1372,11 @@ const ChatPage: React.FC = () => {
|
||||
}
|
||||
|
||||
const handleDraftDiscard = async (): Promise<void> => {
|
||||
if (!activeDraftReview || !currentSessionId || draftDiscardSubmitting) return
|
||||
if (!activeFileReview || !currentSessionId || draftDiscardSubmitting) return
|
||||
setDraftDiscardSubmitting(true)
|
||||
try {
|
||||
const discardedReviewId = activeDraftReview.review.id
|
||||
const response = await window.api.draftReviewDiscard({
|
||||
const discardedReviewId = activeFileReview.review.id
|
||||
const response = await window.api.fileReviewDiscard({
|
||||
reviewId: discardedReviewId
|
||||
})
|
||||
if (!response.success) {
|
||||
@@ -1402,7 +1402,7 @@ const ChatPage: React.FC = () => {
|
||||
setDraftCommitValidation(null)
|
||||
|
||||
if (remainingTabs.length === 0) {
|
||||
setActiveDraftReview(null)
|
||||
setActiveFileReview(null)
|
||||
setContentPanel((current) =>
|
||||
current.kind === 'review_workspace' ? createEmptyPanel() : current
|
||||
)
|
||||
@@ -1411,7 +1411,7 @@ const ChatPage: React.FC = () => {
|
||||
|
||||
const nextTab = remainingTabs[0]
|
||||
showReviewWorkspace(remainingTabs, nextTab.fileKey)
|
||||
await loadDraftReviewDetail({
|
||||
await loadFileReviewDetail({
|
||||
reviewId: nextTab.reviewId,
|
||||
tabs: remainingTabs,
|
||||
activeFileKey: nextTab.fileKey,
|
||||
@@ -1530,7 +1530,7 @@ const ChatPage: React.FC = () => {
|
||||
onOpenFile={(file) => {
|
||||
void openFile(file)
|
||||
}}
|
||||
onOpenReview={openDraftReview}
|
||||
onOpenReview={openFileReview}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1884,8 +1884,8 @@ const ChatPage: React.FC = () => {
|
||||
>
|
||||
<WorkbenchContentPanel
|
||||
state={contentPanel}
|
||||
activeReview={activeDraftReview}
|
||||
draftReviewLoading={draftReviewLoading}
|
||||
activeReview={activeFileReview}
|
||||
fileReviewLoading={fileReviewLoading}
|
||||
draftCommitValidation={draftCommitValidation}
|
||||
draftCommitSubmitting={draftCommitSubmitting}
|
||||
draftDiscardSubmitting={draftDiscardSubmitting}
|
||||
@@ -1898,7 +1898,7 @@ const ChatPage: React.FC = () => {
|
||||
tab.fileKey === fileKey ? { ...tab, unseen: false } : tab
|
||||
)
|
||||
setReviewWorkspaceTabs(nextTabs)
|
||||
void loadDraftReviewDetail({
|
||||
void loadFileReviewDetail({
|
||||
reviewId: nextTab.reviewId,
|
||||
tabs: nextTabs,
|
||||
activeFileKey: fileKey,
|
||||
|
||||
+76
-68
@@ -375,7 +375,7 @@ export type DraftChangeKind =
|
||||
| 'worksheet_range'
|
||||
| 'slide_object'
|
||||
| 'page_region'
|
||||
export type DraftReviewRenderKind =
|
||||
export type FileReviewRenderKind =
|
||||
| 'docx_block'
|
||||
| 'text_hunk'
|
||||
| 'sheet_range'
|
||||
@@ -383,7 +383,7 @@ export type DraftReviewRenderKind =
|
||||
| 'preview_only'
|
||||
export type DraftChangeDecision = 'pending' | 'accepted' | 'rejected'
|
||||
export type DraftChangeResolution = Exclude<DraftChangeDecision, 'pending'>
|
||||
export type DraftReviewStatus =
|
||||
export type FileReviewStatus =
|
||||
| 'pending_confirm'
|
||||
| 'committing'
|
||||
| 'committed'
|
||||
@@ -392,8 +392,8 @@ export type DraftReviewStatus =
|
||||
| 'superseded'
|
||||
| 'failed'
|
||||
export type DraftArtifactRole = 'draft' | 'committed'
|
||||
export type DraftCommitJobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'conflicted'
|
||||
export type DraftCommitValidationIssueCode =
|
||||
export type CommitAttemptStatus = 'pending' | 'running' | 'completed' | 'failed' | 'conflicted'
|
||||
export type CommitValidationIssueCode =
|
||||
| 'source_changed'
|
||||
| 'session_not_found'
|
||||
| 'session_not_ready'
|
||||
@@ -409,13 +409,28 @@ export interface DraftSourceVersion {
|
||||
content_hash?: string
|
||||
}
|
||||
|
||||
export interface DraftReviewSummary {
|
||||
/**
|
||||
* FileReview is the single-file review envelope shared by supported file editors.
|
||||
* Only docx is implemented today. Future xlsx/pptx support should reuse this
|
||||
* outer shape while keeping format-specific change generation and commit rules
|
||||
* inside each file subsystem.
|
||||
*/
|
||||
export interface FileReview {
|
||||
id: string
|
||||
session_id: string
|
||||
run_id: string
|
||||
status: DraftReviewStatus
|
||||
status: FileReviewStatus
|
||||
title: string
|
||||
document_count: number
|
||||
source_document_id?: string
|
||||
source_path: string
|
||||
source_name: string
|
||||
format: DraftDocumentFormat
|
||||
review_render_kind?: FileReviewRenderKind
|
||||
latest_draft_snapshot_id: string
|
||||
draft_artifact_path?: string
|
||||
working_artifact_path?: string
|
||||
committed_artifact_path?: string
|
||||
source_version: DraftSourceVersion
|
||||
change_count: number
|
||||
pending_change_count: number
|
||||
accepted_change_count: number
|
||||
@@ -427,40 +442,18 @@ export interface DraftReviewSummary {
|
||||
last_error?: string
|
||||
}
|
||||
|
||||
export interface DraftReviewDocument {
|
||||
export interface FileReviewChange {
|
||||
id: string
|
||||
review_id: string
|
||||
source_document_id?: string
|
||||
source_path: string
|
||||
source_name: string
|
||||
format: DraftDocumentFormat
|
||||
review_render_kind?: DraftReviewRenderKind
|
||||
latest_draft_snapshot_id: string
|
||||
review_artifact_path?: string
|
||||
working_artifact_path?: string
|
||||
committed_artifact_path?: string
|
||||
source_version: DraftSourceVersion
|
||||
total_change_count: number
|
||||
pending_change_count: number
|
||||
accepted_change_count: number
|
||||
rejected_change_count: number
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export interface DraftReviewChange {
|
||||
id: string
|
||||
review_id: string
|
||||
document_id: string
|
||||
order_index: number
|
||||
change_kind: DraftChangeKind
|
||||
title: string
|
||||
summary: string
|
||||
decision: DraftChangeDecision
|
||||
before_text: string
|
||||
after_text: string
|
||||
before_text?: string
|
||||
after_text?: string
|
||||
anchor_name?: string
|
||||
locator: JsonObject
|
||||
locator?: JsonObject
|
||||
diff_hunks?: DraftTextDiffHunk[]
|
||||
source_step_ref?: string
|
||||
metadata?: JsonObject
|
||||
@@ -480,88 +473,103 @@ export interface DraftTextDiffHunk {
|
||||
lines: DraftTextDiffHunkLine[]
|
||||
}
|
||||
|
||||
export interface DraftCommitResultItem {
|
||||
draft_document_id: string
|
||||
export interface CommitResultItem {
|
||||
review_id: string
|
||||
output_path: string
|
||||
operation: AiFileArtifactOperation
|
||||
checksum?: string
|
||||
byte_size?: number
|
||||
}
|
||||
|
||||
export interface DraftCommitJob {
|
||||
/**
|
||||
* CommitAttempt records submit-to-source executions for a FileReview.
|
||||
* It is lifecycle history, not change content.
|
||||
*/
|
||||
export interface CommitAttempt {
|
||||
id: string
|
||||
review_id: string
|
||||
status: DraftCommitJobStatus
|
||||
document_count: number
|
||||
results: DraftCommitResultItem[]
|
||||
status: CommitAttemptStatus
|
||||
results: CommitResultItem[]
|
||||
started_at: number
|
||||
completed_at?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface DraftReviewDetail {
|
||||
review: DraftReviewSummary
|
||||
documents: DraftReviewDocument[]
|
||||
changes: DraftReviewChange[]
|
||||
commit_jobs: DraftCommitJob[]
|
||||
export interface FileReviewDetail {
|
||||
review: FileReview
|
||||
changes: FileReviewChange[]
|
||||
commit_attempts: CommitAttempt[]
|
||||
}
|
||||
|
||||
export interface ListDraftReviewsRequest {
|
||||
export interface ListFileReviewsRequest {
|
||||
sessionId?: string
|
||||
status?: DraftReviewStatus
|
||||
status?: FileReviewStatus
|
||||
}
|
||||
|
||||
export interface ListDraftReviewsResponse {
|
||||
items: DraftReviewSummary[]
|
||||
export interface ListFileReviewsResponse {
|
||||
items: FileReview[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface GetDraftReviewRequest {
|
||||
export interface GetFileReviewRequest {
|
||||
reviewId: string
|
||||
}
|
||||
|
||||
export interface GetDraftReviewResponse {
|
||||
review?: DraftReviewDetail
|
||||
export interface GetFileReviewResponse {
|
||||
review?: FileReviewDetail
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface DraftCommitValidationIssue {
|
||||
code: DraftCommitValidationIssueCode
|
||||
export interface CommitValidationIssue {
|
||||
code: CommitValidationIssueCode
|
||||
message: string
|
||||
documentId?: string
|
||||
sourcePath?: string
|
||||
}
|
||||
|
||||
export interface ValidateDraftCommitRequest {
|
||||
export interface ValidateFileReviewCommitRequest {
|
||||
reviewId: string
|
||||
}
|
||||
|
||||
export interface ValidateDraftCommitResponse {
|
||||
export interface ValidateFileReviewCommitResponse {
|
||||
ready: boolean
|
||||
issues: DraftCommitValidationIssue[]
|
||||
issues: CommitValidationIssue[]
|
||||
}
|
||||
|
||||
export interface CommitDraftReviewRequest {
|
||||
export interface CommitFileReviewRequest {
|
||||
reviewId: string
|
||||
overwrite?: boolean
|
||||
}
|
||||
|
||||
export interface CommitDraftReviewResponse {
|
||||
export interface CommitFileReviewResponse {
|
||||
success: boolean
|
||||
job?: DraftCommitJob
|
||||
job?: CommitAttempt
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface DiscardDraftReviewRequest {
|
||||
export interface DiscardFileReviewRequest {
|
||||
reviewId: string
|
||||
}
|
||||
|
||||
export interface DiscardDraftReviewResponse {
|
||||
export interface DiscardFileReviewResponse {
|
||||
success: boolean
|
||||
review?: DraftReviewSummary
|
||||
review?: FileReview
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface RuntimeChangedFile {
|
||||
file_name: string
|
||||
source_path: string
|
||||
format: 'docx'
|
||||
change_count: number
|
||||
summary: string
|
||||
change_highlights: string[]
|
||||
draft_artifact_path?: string
|
||||
}
|
||||
|
||||
export interface RuntimeArtifactsSummary {
|
||||
changed_files: RuntimeChangedFile[]
|
||||
}
|
||||
|
||||
export interface PickApplicationResponse {
|
||||
canceled: boolean
|
||||
appPath?: string
|
||||
@@ -677,17 +685,17 @@ export type AiDraftSnapshotRef = {
|
||||
format?: DraftDocumentFormat
|
||||
sourcePath?: string
|
||||
}
|
||||
export type AiDraftReviewRef = {
|
||||
export type AiFileReviewRef = {
|
||||
reviewId: string
|
||||
documentCount: number
|
||||
changeCount: number
|
||||
}
|
||||
export type AiDraftReviewArtifactRef = {
|
||||
export type AiFileReviewArtifactRef = {
|
||||
kind: 'draft_review'
|
||||
review: AiDraftReviewRef
|
||||
review: AiFileReviewRef
|
||||
summary: string
|
||||
}
|
||||
export type AiArtifactRef = AiFileArtifactRef | AiDraftReviewArtifactRef
|
||||
export type AiArtifactRef = AiFileArtifactRef | AiFileReviewArtifactRef
|
||||
|
||||
export type AiRunStartRequest = {
|
||||
sessionId: string
|
||||
@@ -860,7 +868,7 @@ type AiRunEventPayload =
|
||||
| {
|
||||
type: 'draft_review_ready'
|
||||
runId: string
|
||||
review: AiDraftReviewRef
|
||||
review: AiFileReviewRef
|
||||
createdAt: number
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
collectDocxApplyOpsContentKinds,
|
||||
filterAiRunEventsByType,
|
||||
hasDraftFileArtifact,
|
||||
hasDraftReviewArtifact,
|
||||
hasFileReviewArtifact,
|
||||
runWithStructuredOutputRetry
|
||||
} from '../testUtils'
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('ai human live backend', () => {
|
||||
) &&
|
||||
changedContentKinds.includes('paragraph') &&
|
||||
filterAiRunEventsByType(events, 'draft_review_ready').length >= 1 &&
|
||||
hasDraftReviewArtifact(result.output?.artifacts) &&
|
||||
hasFileReviewArtifact(result.output?.artifacts) &&
|
||||
hasDraftFileArtifact(result.output?.artifacts) &&
|
||||
unchangedFixtures.every(Boolean) &&
|
||||
/(过拟合|过度拟合)/.test(deliveryText) &&
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
collectDocxApplyOpsContentKinds,
|
||||
filterAiRunEventsByType,
|
||||
hasDraftFileArtifact,
|
||||
hasDraftReviewArtifact,
|
||||
hasFileReviewArtifact,
|
||||
runWithStructuredOutputRetry
|
||||
} from '../testUtils'
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('ai human live backend', () => {
|
||||
changedContentKinds.includes('paragraph') &&
|
||||
changedContentKinds.includes('table') &&
|
||||
filterAiRunEventsByType(events, 'draft_review_ready').length >= 1 &&
|
||||
hasDraftReviewArtifact(result.output?.artifacts) &&
|
||||
hasFileReviewArtifact(result.output?.artifacts) &&
|
||||
hasDraftFileArtifact(result.output?.artifacts) &&
|
||||
unchangedFixtures.every(Boolean) &&
|
||||
Boolean(targetFixture) &&
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '../../../harness/backend/aiLiveTestData'
|
||||
import {
|
||||
countDraftFileArtifacts,
|
||||
countDraftReviewArtifacts,
|
||||
countFileReviewArtifacts,
|
||||
collectToolDiagnosticMessages,
|
||||
filterAiRunEventsByType,
|
||||
runWithStructuredOutputRetry
|
||||
@@ -133,7 +133,7 @@ describe('ai human live backend', () => {
|
||||
successfulWriteResults.length === 0 &&
|
||||
failedWriteResults.length >= 1 &&
|
||||
diagnosticMessages.some((message) => message.includes('当前命中复杂定位排版区域')) &&
|
||||
countDraftReviewArtifacts(result.output?.artifacts) === 0 &&
|
||||
countFileReviewArtifacts(result.output?.artifacts) === 0 &&
|
||||
countDraftFileArtifacts(result.output?.artifacts) === 0 &&
|
||||
/(手动|版式|排版|没法自动|无法自动|复杂定位)/.test(deliveryText) &&
|
||||
unchangedFixtures.every(Boolean) &&
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import {
|
||||
collectDocxApplyOpsContentKinds,
|
||||
countDraftFileArtifacts,
|
||||
countDraftReviewArtifacts,
|
||||
countFileReviewArtifacts,
|
||||
filterAiRunEventsByType,
|
||||
hasKbToolCall,
|
||||
runWithStructuredOutputRetry
|
||||
@@ -120,7 +120,7 @@ describe('ai human live backend', () => {
|
||||
sourceDocsOpened &&
|
||||
targetDocsWritten &&
|
||||
filterAiRunEventsByType(events, 'draft_review_ready').length >= 2 &&
|
||||
countDraftReviewArtifacts(result.output?.artifacts) >= 2 &&
|
||||
countFileReviewArtifacts(result.output?.artifacts) >= 2 &&
|
||||
countDraftFileArtifacts(result.output?.artifacts) >= 2 &&
|
||||
/(过拟合|过度拟合)/.test(deliveryText) &&
|
||||
/反走样/.test(deliveryText) &&
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
filterAiRunEventsByType,
|
||||
hasDocxWriteToolCall,
|
||||
hasDraftFileArtifact,
|
||||
hasDraftReviewArtifact,
|
||||
hasFileReviewArtifact,
|
||||
hasKbToolCall
|
||||
} from '../testUtils'
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('ai human live backend', () => {
|
||||
kbEvidenceText.includes('Owner Note.md') &&
|
||||
deliveryText.length > 0 &&
|
||||
filterAiRunEventsByType(result.events, 'draft_review_ready').length >= 1 &&
|
||||
hasDraftReviewArtifact(result.output?.artifacts) &&
|
||||
hasFileReviewArtifact(result.output?.artifacts) &&
|
||||
hasDraftFileArtifact(result.output?.artifacts),
|
||||
{
|
||||
name: 'human-live-risk-report-delivery-is-backed-by-evidence-and-review-artifacts',
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('ai human mock-llm backend', () => {
|
||||
ensureReadyCalls += 1
|
||||
await runtime.ensureEmbeddingReady()
|
||||
},
|
||||
draftReviewService: runtime.draftReviewService,
|
||||
fileReviewService: runtime.fileReviewService,
|
||||
isCancelled: () => false,
|
||||
sendEvent: (event) => runtime.eventCollector.add(event),
|
||||
recordLlmIo: (record) => runtime.eventCollector.addLlmIo(record),
|
||||
@@ -170,7 +170,7 @@ describe('ai human mock-llm backend', () => {
|
||||
ensureEmbeddingReady: async () => {
|
||||
throw new Error('embedding preflight failed for test')
|
||||
},
|
||||
draftReviewService: runtime.draftReviewService,
|
||||
fileReviewService: runtime.fileReviewService,
|
||||
isCancelled: () => false,
|
||||
sendEvent: (event) => runtime.eventCollector.add(event),
|
||||
recordLlmIo: (record) => runtime.eventCollector.addLlmIo(record),
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('ai human mock-llm backend', () => {
|
||||
ensureEmbeddingReady: async () => {
|
||||
await runtime.ensureEmbeddingReady()
|
||||
},
|
||||
draftReviewService: runtime.draftReviewService,
|
||||
fileReviewService: runtime.fileReviewService,
|
||||
isCancelled: () => false,
|
||||
sendEvent: (event) => runtime.eventCollector.add(event),
|
||||
recordLlmIo: (record) => runtime.eventCollector.addLlmIo(record),
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
collectDocxApplyOpsContentKinds,
|
||||
filterAiRunEventsByType,
|
||||
hasDraftFileArtifact,
|
||||
hasDraftReviewArtifact,
|
||||
hasFileReviewArtifact,
|
||||
readLatestToolResultDataFromEvents
|
||||
} from '../testUtils'
|
||||
import { findParagraphBlockIdByText, type DocxOutlineResult } from '../../tool/docx/testUtils'
|
||||
@@ -139,7 +139,7 @@ describe('ai human mock-llm backend', () => {
|
||||
applyResult.changedBlocks.length === 1 &&
|
||||
searchResult.matches.length === 1 &&
|
||||
readResult.blocks.length === 1 &&
|
||||
hasDraftReviewArtifact(result.output.artifacts) &&
|
||||
hasFileReviewArtifact(result.output.artifacts) &&
|
||||
hasDraftFileArtifact(result.output.artifacts) &&
|
||||
filterAiRunEventsByType(result.events, 'draft_review_ready').length === 1,
|
||||
{
|
||||
@@ -317,7 +317,7 @@ describe('ai human mock-llm backend', () => {
|
||||
tableEditResult.changedBlocks.some((block) => block.action === 'insert_after') &&
|
||||
contentKinds.includes('paragraph') &&
|
||||
contentKinds.includes('table') &&
|
||||
hasDraftReviewArtifact(result.output.artifacts) &&
|
||||
hasFileReviewArtifact(result.output.artifacts) &&
|
||||
hasDraftFileArtifact(result.output.artifacts) &&
|
||||
outlineResult.blocks.some(
|
||||
(block) =>
|
||||
|
||||
@@ -115,10 +115,10 @@ export const collectStartedToolStringArgs = (
|
||||
export const hasKbToolCall = (events: readonly AiRunEvent[]): boolean =>
|
||||
filterAiRunEventsByType(events, 'tool_call_result').some((event) => event.toolName.startsWith('kb_'))
|
||||
|
||||
export const hasDraftReviewArtifact = (artifacts: readonly AiArtifactRef[] | undefined): boolean =>
|
||||
export const hasFileReviewArtifact = (artifacts: readonly AiArtifactRef[] | undefined): boolean =>
|
||||
Array.isArray(artifacts) && artifacts.some((artifact) => artifact.kind === 'draft_review')
|
||||
|
||||
export const countDraftReviewArtifacts = (artifacts: readonly AiArtifactRef[] | undefined): number =>
|
||||
export const countFileReviewArtifacts = (artifacts: readonly AiArtifactRef[] | undefined): number =>
|
||||
Array.isArray(artifacts) ? artifacts.filter((artifact) => artifact.kind === 'draft_review').length : 0
|
||||
|
||||
export const hasDraftFileArtifact = (artifacts: readonly AiArtifactRef[] | undefined): boolean =>
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('ai tool docx backend', () => {
|
||||
{ type: 'paragraph', text: '结尾段落。' }
|
||||
])
|
||||
|
||||
runtime.draftReviewService.beginRun({
|
||||
runtime.fileReviewService.beginRun({
|
||||
runId: runtime.runId,
|
||||
sessionId: 'docx-review-order-session',
|
||||
inputText: '追加解释和对照表'
|
||||
@@ -61,16 +61,16 @@ describe('ai tool docx backend', () => {
|
||||
]
|
||||
})
|
||||
|
||||
const details = await runtime.draftReviewService.finalizeRunReview({
|
||||
const details = await runtime.fileReviewService.finalizeRunReview({
|
||||
runId: runtime.runId,
|
||||
workspaceRootPath: runtime.workspaceRoot
|
||||
})
|
||||
const document = details[0]?.documents[0]
|
||||
if (!document?.review_artifact_path) {
|
||||
const review = details[0]?.review
|
||||
if (!review?.draft_artifact_path) {
|
||||
throw new Error('expected review artifact path')
|
||||
}
|
||||
|
||||
const reviewBuffer = await fs.readFile(document.review_artifact_path)
|
||||
const reviewBuffer = await fs.readFile(review.draft_artifact_path)
|
||||
const reviewBundle = await loadDocxBundle(reviewBuffer)
|
||||
const reviewOutline = buildBlockIndex(reviewBundle.documentDom).outline
|
||||
.filter((block) => block.type !== 'table_cell')
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('ai tool docx backend', () => {
|
||||
}
|
||||
])
|
||||
|
||||
runtime.draftReviewService.beginRun({
|
||||
runtime.fileReviewService.beginRun({
|
||||
runId: runtime.runId,
|
||||
sessionId: 'docx-review-projection-session',
|
||||
inputText: '补写解释和表格'
|
||||
@@ -65,22 +65,21 @@ describe('ai tool docx backend', () => {
|
||||
['平滑边缘,降低锯齿', '记住训练噪声,泛化变差']
|
||||
]
|
||||
})
|
||||
const details = await runtime.draftReviewService.finalizeRunReview({
|
||||
const details = await runtime.fileReviewService.finalizeRunReview({
|
||||
runId: runtime.runId,
|
||||
workspaceRootPath: runtime.workspaceRoot
|
||||
})
|
||||
const detail = details[0]
|
||||
const document = detail?.documents[0]
|
||||
if (!detail || !document?.review_artifact_path || !document.working_artifact_path) {
|
||||
if (!detail || !detail.review.draft_artifact_path || !detail.review.working_artifact_path) {
|
||||
throw new Error('expected generated review artifacts')
|
||||
}
|
||||
|
||||
const reviewBuffer = await fs.readFile(document.review_artifact_path)
|
||||
const reviewBuffer = await fs.readFile(detail.review.draft_artifact_path)
|
||||
const reviewBundle = await loadDocxBundle(reviewBuffer)
|
||||
const reviewXml = new XMLSerializer().serializeToString(reviewBundle.documentDom)
|
||||
const table = descendantElements(reviewBundle.documentDom.documentElement, 'tbl')[0]
|
||||
const workingExists = await fs
|
||||
.access(document.working_artifact_path)
|
||||
.access(detail.review.working_artifact_path)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
@@ -88,8 +87,8 @@ describe('ai tool docx backend', () => {
|
||||
outline,
|
||||
tableInsert,
|
||||
reviewId: detail.review.id,
|
||||
reviewArtifactPath: document.review_artifact_path,
|
||||
workingArtifactPath: document.working_artifact_path,
|
||||
reviewArtifactPath: detail.review.draft_artifact_path,
|
||||
workingArtifactPath: detail.review.working_artifact_path,
|
||||
reviewXml,
|
||||
reviewHasNativeRevisions:
|
||||
descendantElements(reviewBundle.documentDom.documentElement, 'ins').length > 0 ||
|
||||
@@ -109,8 +108,8 @@ describe('ai tool docx backend', () => {
|
||||
runtime.assertThat(
|
||||
result.tableInsert.changedBlocks.length === 1 &&
|
||||
result.workingExists &&
|
||||
result.reviewArtifactPath.includes('-review-') &&
|
||||
result.workingArtifactPath.includes('-working-'),
|
||||
result.reviewArtifactPath.length > 0 &&
|
||||
result.workingArtifactPath.length > 0,
|
||||
{
|
||||
name: 'review-run-emits-working-and-review-artifacts',
|
||||
message: 'docx review finalize 后应同时产出 working artifact 与 review artifact。',
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('ai tool docx backend', () => {
|
||||
}
|
||||
])
|
||||
|
||||
await runtime.seedDraftReview({
|
||||
await runtime.seedFileReview({
|
||||
sessionId: 'review-aware-session',
|
||||
runId: runtime.runId,
|
||||
title: 'Review aware docx seed',
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('ai tool docx backend', () => {
|
||||
{ type: 'paragraph', text: '待修改的原文。' }
|
||||
])
|
||||
|
||||
runtime.draftReviewService.beginRun({
|
||||
runtime.fileReviewService.beginRun({
|
||||
runId: runtime.runId,
|
||||
sessionId: 'commit-conflict-session',
|
||||
inputText: '准备后制造冲突'
|
||||
@@ -36,7 +36,7 @@ describe('ai tool docx backend', () => {
|
||||
query: '待修改的原文。',
|
||||
replace: '工作稿中的新版本。'
|
||||
})
|
||||
const details = await runtime.draftReviewService.finalizeRunReview({
|
||||
const details = await runtime.fileReviewService.finalizeRunReview({
|
||||
runId: runtime.runId,
|
||||
workspaceRootPath: runtime.workspaceRoot
|
||||
})
|
||||
@@ -50,14 +50,14 @@ describe('ai tool docx backend', () => {
|
||||
])
|
||||
const externalSourceBuffer = await fs.readFile(`${runtime.workspaceRoot}/commit-conflict.docx`)
|
||||
|
||||
const validation = await runtime.draftReviewService.validateCommit({
|
||||
const validation = await runtime.fileReviewService.validateCommit({
|
||||
reviewId: detail.review.id
|
||||
})
|
||||
const commit = await runtime.draftReviewService.commitReview({
|
||||
const commit = await runtime.fileReviewService.commitReview({
|
||||
reviewId: detail.review.id,
|
||||
overwrite: true
|
||||
})
|
||||
const conflictedDetail = await runtime.draftReviewStore.getReviewDetail(detail.review.id)
|
||||
const conflictedDetail = await runtime.fileReviewStore.getReviewDetail(detail.review.id)
|
||||
const sourceBuffer = await fs.readFile(`${runtime.workspaceRoot}/commit-conflict.docx`)
|
||||
|
||||
return {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('ai tool docx backend', () => {
|
||||
}
|
||||
])
|
||||
|
||||
const seeded = await runtime.seedDraftReview({
|
||||
const seeded = await runtime.seedFileReview({
|
||||
sessionId: 'commit-working-session',
|
||||
runId: runtime.runId,
|
||||
title: 'Commit uses committed artifact',
|
||||
@@ -58,9 +58,9 @@ describe('ai tool docx backend', () => {
|
||||
]
|
||||
})
|
||||
|
||||
const detail = await runtime.draftReviewStore.getReviewDetail(seeded.reviewId)
|
||||
const document = detail?.documents[0]
|
||||
if (!document?.working_artifact_path || !document.committed_artifact_path) {
|
||||
const detail = await runtime.fileReviewStore.getReviewDetail(seeded.reviewId)
|
||||
const review = detail?.review
|
||||
if (!review?.working_artifact_path || !review.committed_artifact_path) {
|
||||
throw new Error('expected seeded working/committed artifact paths')
|
||||
}
|
||||
|
||||
@@ -76,11 +76,11 @@ describe('ai tool docx backend', () => {
|
||||
])
|
||||
|
||||
const workingSourceBuffer = await fs.readFile(`${runtime.workspaceRoot}/commit-working.docx`)
|
||||
await fs.writeFile(document.working_artifact_path, workingSourceBuffer)
|
||||
await fs.writeFile(review.working_artifact_path, workingSourceBuffer)
|
||||
const committedSourceBuffer = await fs.readFile(`${runtime.workspaceRoot}/commit-committed.docx`)
|
||||
await fs.writeFile(document.committed_artifact_path, committedSourceBuffer)
|
||||
await fs.writeFile(review.committed_artifact_path, committedSourceBuffer)
|
||||
|
||||
const commit = await runtime.draftReviewService.commitReview({
|
||||
const commit = await runtime.fileReviewService.commitReview({
|
||||
reviewId: seeded.reviewId,
|
||||
overwrite: true
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('ai tool docx backend', () => {
|
||||
])
|
||||
const sourceBufferBefore = await fs.readFile(sourcePath)
|
||||
|
||||
runtime.draftReviewService.beginRun({
|
||||
runtime.fileReviewService.beginRun({
|
||||
runId: runtime.runId,
|
||||
sessionId: 'discard-review-session',
|
||||
inputText: '创建后再放弃'
|
||||
@@ -39,17 +39,16 @@ describe('ai tool docx backend', () => {
|
||||
query: '源文档原始内容。',
|
||||
replace: '仅存在于工作稿中的内容。'
|
||||
})
|
||||
const details = await runtime.draftReviewService.finalizeRunReview({
|
||||
const details = await runtime.fileReviewService.finalizeRunReview({
|
||||
runId: runtime.runId,
|
||||
workspaceRootPath: runtime.workspaceRoot
|
||||
})
|
||||
const detail = details[0]
|
||||
const document = detail?.documents[0]
|
||||
if (!detail || !document?.review_artifact_path || !document.working_artifact_path) {
|
||||
if (!detail || !detail.review.draft_artifact_path || !detail.review.working_artifact_path) {
|
||||
throw new Error('expected finalized review detail')
|
||||
}
|
||||
|
||||
const discard = await runtime.draftReviewService.discardReview({
|
||||
const discard = await runtime.fileReviewService.discardReview({
|
||||
reviewId: detail.review.id
|
||||
})
|
||||
const sourceBufferAfter = await fs.readFile(sourcePath)
|
||||
@@ -65,14 +64,14 @@ describe('ai tool docx backend', () => {
|
||||
})
|
||||
await freshHarness.release()
|
||||
const reviewArtifactExists = await fs
|
||||
.access(document.review_artifact_path)
|
||||
.access(detail.review.draft_artifact_path)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
const workingArtifactExists = await fs
|
||||
.access(document.working_artifact_path)
|
||||
.access(detail.review.working_artifact_path)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
const discardedDetail = await runtime.draftReviewStore.getReviewDetail(detail.review.id)
|
||||
const discardedDetail = await runtime.fileReviewStore.getReviewDetail(detail.review.id)
|
||||
|
||||
return {
|
||||
changed,
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('ai tool docx backend', () => {
|
||||
runMode: 'single',
|
||||
execute: async (runtime) => {
|
||||
const workspaceSeed = await seedRiskReportWorkspace(runtime)
|
||||
runtime.draftReviewService.beginRun({
|
||||
runtime.fileReviewService.beginRun({
|
||||
runId: runtime.runId,
|
||||
sessionId: 'docx-risk-report-review-commit-session',
|
||||
inputText: '补全风险周报模板并提交'
|
||||
@@ -43,17 +43,16 @@ describe('ai tool docx backend', () => {
|
||||
replace: '最终复核窗口由合规负责人 Alice 牵头。'
|
||||
})
|
||||
|
||||
const details = await runtime.draftReviewService.finalizeRunReview({
|
||||
const details = await runtime.fileReviewService.finalizeRunReview({
|
||||
runId: runtime.runId,
|
||||
workspaceRootPath: runtime.workspaceRoot
|
||||
})
|
||||
const detail = details[0]
|
||||
const document = detail?.documents[0]
|
||||
if (!detail || !document?.review_artifact_path) {
|
||||
if (!detail || !detail.review.draft_artifact_path) {
|
||||
throw new Error('expected generated review artifact for risk report')
|
||||
}
|
||||
|
||||
const reviewRelativePath = path.relative(runtime.workspaceRoot, document.review_artifact_path)
|
||||
const reviewRelativePath = path.relative(runtime.workspaceRoot, detail.review.draft_artifact_path)
|
||||
const reviewRisk = await harness.invoke<DocxSearchResult>('docx_search', {
|
||||
sourcePath: reviewRelativePath,
|
||||
query: '待跟进'
|
||||
@@ -71,10 +70,10 @@ describe('ai tool docx backend', () => {
|
||||
query: 'OWNER_TODO'
|
||||
})
|
||||
const reviewXml = new XMLSerializer().serializeToString(
|
||||
(await loadDocxBundle(await fs.readFile(document.review_artifact_path))).documentDom
|
||||
(await loadDocxBundle(await fs.readFile(detail.review.draft_artifact_path))).documentDom
|
||||
)
|
||||
|
||||
const commit = await runtime.draftReviewService.commitReview({
|
||||
const commit = await runtime.fileReviewService.commitReview({
|
||||
reviewId: detail.review.id,
|
||||
overwrite: true
|
||||
})
|
||||
@@ -101,7 +100,7 @@ describe('ai tool docx backend', () => {
|
||||
firstEdit,
|
||||
secondEdit,
|
||||
reviewId: detail.review.id,
|
||||
reviewArtifactPath: document.review_artifact_path,
|
||||
reviewArtifactPath: detail.review.draft_artifact_path,
|
||||
reviewRisk,
|
||||
reviewOwner,
|
||||
reviewOldRiskPlaceholder,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
import { createHumanGraphRunner } from '../../../src/main/services/ai/human/graph/humanGraph'
|
||||
import { DraftReviewService } from '../../../src/main/services/ai/human/tools/docx/review/DraftReviewService'
|
||||
import { FileReviewService } from '../../../src/main/services/ai/human/tools/docx/review/FileReviewService'
|
||||
import { createAiRunContext } from '../../../src/main/services/ai/runtime'
|
||||
import { runAiRuntimePreflight } from '../../../src/main/services/ai/runtimePreflight'
|
||||
import type { AiApprovalDecision, AiApprovalRequest } from '../../../src/main/services/ai/types'
|
||||
@@ -44,7 +44,7 @@ const buildApprovalResolver = (
|
||||
export type RealAiDepsFactory = {
|
||||
storageService: StorageService
|
||||
embeddingService: EmbeddingService
|
||||
draftReviewService: DraftReviewService
|
||||
fileReviewService: FileReviewService
|
||||
ensureEmbeddingReady: () => Promise<void>
|
||||
createAiRunContext: (input?: CreateAiRunContextInput) => AiRunContext
|
||||
createHumanTaskRunner: (input?: CreateAiRunContextInput) => ReturnType<typeof createHumanGraphRunner>
|
||||
@@ -66,8 +66,8 @@ export const createRealAiDepsFactory = async (input: {
|
||||
const embeddingService = new EmbeddingService()
|
||||
const modelService = ModelService.getInstance()
|
||||
|
||||
const draftReviewService = new DraftReviewService(
|
||||
storageService.draftReview,
|
||||
const fileReviewService = new FileReviewService(
|
||||
storageService.fileReview,
|
||||
storageService.documents
|
||||
)
|
||||
|
||||
@@ -120,7 +120,7 @@ export const createRealAiDepsFactory = async (input: {
|
||||
documentsStore: storageService.documents,
|
||||
embeddingService,
|
||||
ensureEmbeddingReady: ensureReady,
|
||||
draftReviewService,
|
||||
fileReviewService,
|
||||
isCancelled: contextInput.checkCancelledOverride || (() => false),
|
||||
sendEvent: (event) => input.eventCollector.add(event),
|
||||
recordLlmIo: (record) => input.eventCollector.addLlmIo(record),
|
||||
@@ -169,7 +169,7 @@ export const createRealAiDepsFactory = async (input: {
|
||||
return {
|
||||
storageService,
|
||||
embeddingService,
|
||||
draftReviewService,
|
||||
fileReviewService,
|
||||
ensureEmbeddingReady: ensureReady,
|
||||
createAiRunContext: createContext,
|
||||
createHumanTaskRunner: (contextInput = {}) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { FixtureBinding } from './test-types'
|
||||
import { createRealAiDepsFactory } from './realAiDepsFactory'
|
||||
import { seedKbFixture } from './seedKbFixture'
|
||||
import { seedChatHistory } from './seedChatHistory'
|
||||
import { seedDraftReview } from './seedDraftReview'
|
||||
import { seedFileReview } from './seedFileReview'
|
||||
import {
|
||||
appendSeedKbDocuments,
|
||||
appendSeedWorkspaceFile,
|
||||
@@ -50,10 +50,10 @@ export const createRealAiHarness = async (input: {
|
||||
storageService: depsFactory.storageService,
|
||||
documentsStore: depsFactory.storageService.documents,
|
||||
chatStore: depsFactory.storageService.chat,
|
||||
draftReviewStore: depsFactory.storageService.draftReview,
|
||||
fileReviewStore: depsFactory.storageService.fileReview,
|
||||
llmStore: depsFactory.storageService.llm,
|
||||
embeddingService: depsFactory.embeddingService,
|
||||
draftReviewService: depsFactory.draftReviewService,
|
||||
fileReviewService: depsFactory.fileReviewService,
|
||||
recordEvent: input.recordEvent,
|
||||
assertThat: (condition, assertion) => {
|
||||
input.recordEvent({
|
||||
@@ -101,10 +101,10 @@ export const createRealAiHarness = async (input: {
|
||||
payload
|
||||
})
|
||||
},
|
||||
seedDraftReview: async (payload) =>
|
||||
await seedDraftReview({
|
||||
seedFileReview: async (payload) =>
|
||||
await seedFileReview({
|
||||
workspaceRoot: input.workspaceRoot,
|
||||
draftReviewStore: depsFactory.storageService.draftReview,
|
||||
fileReviewStore: depsFactory.storageService.fileReview,
|
||||
payload
|
||||
}),
|
||||
seedLlmConfig: async (config) => {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type { DraftReviewStore } from '../../../src/main/services/storage/domains/DraftReviewStore'
|
||||
import { buildSourceVersion } from '../../../src/main/services/ai/human/tools/docx/review/utils'
|
||||
import { buildDocxReviewArtifactPaths } from '../../../src/main/services/ai/human/tools/docx/review/viewProjection'
|
||||
import type { SeedDraftReviewInput } from './test-types'
|
||||
|
||||
export const seedDraftReview = async (input: {
|
||||
workspaceRoot: string
|
||||
draftReviewStore: DraftReviewStore
|
||||
payload: SeedDraftReviewInput
|
||||
}): Promise<{ reviewId: string }> => {
|
||||
const reviewId = input.payload.reviewId || randomUUID()
|
||||
const createdAt = input.payload.createdAt || Date.now()
|
||||
const updatedAt = input.payload.updatedAt || createdAt
|
||||
const reviewArtifactsRoot = path.join(
|
||||
input.workspaceRoot,
|
||||
'.ncurator-test',
|
||||
'draft-reviews',
|
||||
reviewId
|
||||
)
|
||||
await fs.mkdir(reviewArtifactsRoot, { recursive: true })
|
||||
|
||||
const documents = await Promise.all(
|
||||
input.payload.documents.map(async (document, index) => {
|
||||
const documentId = `${reviewId}-doc-${index + 1}`
|
||||
const sourceVersion = await buildSourceVersion(document.sourcePath)
|
||||
const confirmSourcePath = document.confirmBasePath || document.sourcePath
|
||||
const {
|
||||
reviewArtifactPath: confirmArtifactPath,
|
||||
workingArtifactPath: effectiveDraftArtifactPath,
|
||||
committedArtifactPath
|
||||
} =
|
||||
buildDocxReviewArtifactPaths({
|
||||
workspaceRootPath: input.workspaceRoot,
|
||||
reviewId,
|
||||
documentIndex: index,
|
||||
sourceName: document.sourceName || path.basename(document.sourcePath)
|
||||
})
|
||||
await fs.mkdir(path.dirname(effectiveDraftArtifactPath), { recursive: true })
|
||||
await fs.copyFile(confirmSourcePath, effectiveDraftArtifactPath)
|
||||
await fs.copyFile(confirmSourcePath, confirmArtifactPath)
|
||||
await fs.copyFile(confirmSourcePath, committedArtifactPath)
|
||||
|
||||
return {
|
||||
id: documentId,
|
||||
review_id: reviewId,
|
||||
source_document_id: document.sourceDocumentId,
|
||||
source_path: document.sourcePath,
|
||||
source_name: document.sourceName || path.basename(document.sourcePath),
|
||||
format: document.format || 'docx',
|
||||
latest_draft_snapshot_id: document.latestDraftSnapshotId || `${documentId}-snapshot`,
|
||||
review_artifact_path: confirmArtifactPath,
|
||||
working_artifact_path: effectiveDraftArtifactPath,
|
||||
committed_artifact_path: committedArtifactPath,
|
||||
source_version: sourceVersion,
|
||||
total_change_count: 0,
|
||||
pending_change_count: 0,
|
||||
accepted_change_count: 0,
|
||||
rejected_change_count: 0,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await input.draftReviewStore.saveReviewDetail({
|
||||
review: {
|
||||
id: reviewId,
|
||||
session_id: input.payload.sessionId || 'test-session',
|
||||
run_id: input.payload.runId || 'test-run',
|
||||
status: input.payload.status || 'pending_confirm',
|
||||
title: input.payload.title || 'Test Draft Review',
|
||||
document_count: documents.length,
|
||||
change_count: 0,
|
||||
pending_change_count: 0,
|
||||
accepted_change_count: 0,
|
||||
rejected_change_count: 0,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt
|
||||
},
|
||||
documents,
|
||||
changes: [],
|
||||
commit_jobs: []
|
||||
})
|
||||
|
||||
return { reviewId }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type { FileReviewStore } from '../../../src/main/services/storage/domains/FileReviewStore'
|
||||
import { buildSourceVersion } from '../../../src/main/services/ai/human/tools/docx/review/utils'
|
||||
import { buildDocxReviewArtifactPaths } from '../../../src/main/services/ai/human/tools/docx/review/viewProjection'
|
||||
import type { SeedFileReviewInput } from './test-types'
|
||||
|
||||
export const seedFileReview = async (input: {
|
||||
workspaceRoot: string
|
||||
fileReviewStore: FileReviewStore
|
||||
payload: SeedFileReviewInput
|
||||
}): Promise<{ reviewId: string }> => {
|
||||
const reviewId = input.payload.reviewId || randomUUID()
|
||||
const createdAt = input.payload.createdAt || Date.now()
|
||||
const updatedAt = input.payload.updatedAt || createdAt
|
||||
const document = input.payload.documents[0]
|
||||
if (!document) {
|
||||
throw new Error('seedFileReview requires one document payload')
|
||||
}
|
||||
|
||||
const sourceVersion = await buildSourceVersion(document.sourcePath)
|
||||
const confirmSourcePath = document.confirmBasePath || document.sourcePath
|
||||
const { draftArtifactPath, workingArtifactPath, committedArtifactPath } =
|
||||
buildDocxReviewArtifactPaths({
|
||||
workspaceRootPath: input.workspaceRoot,
|
||||
reviewId,
|
||||
sourceName: document.sourceName || path.basename(document.sourcePath)
|
||||
})
|
||||
await fs.mkdir(path.dirname(workingArtifactPath), { recursive: true })
|
||||
await fs.copyFile(confirmSourcePath, workingArtifactPath)
|
||||
await fs.copyFile(confirmSourcePath, draftArtifactPath)
|
||||
await fs.copyFile(confirmSourcePath, committedArtifactPath)
|
||||
|
||||
await input.fileReviewStore.saveReviewDetail({
|
||||
review: {
|
||||
id: reviewId,
|
||||
session_id: input.payload.sessionId || 'test-session',
|
||||
run_id: input.payload.runId || 'test-run',
|
||||
status: input.payload.status || 'pending_confirm',
|
||||
title: input.payload.title || 'Test File Review',
|
||||
source_document_id: document.sourceDocumentId,
|
||||
source_path: document.sourcePath,
|
||||
source_name: document.sourceName || path.basename(document.sourcePath),
|
||||
format: document.format || 'docx',
|
||||
review_render_kind: 'docx_block',
|
||||
latest_draft_snapshot_id: document.latestDraftSnapshotId || `${reviewId}-snapshot`,
|
||||
draft_artifact_path: draftArtifactPath,
|
||||
working_artifact_path: workingArtifactPath,
|
||||
committed_artifact_path: committedArtifactPath,
|
||||
source_version: sourceVersion,
|
||||
change_count: 0,
|
||||
pending_change_count: 0,
|
||||
accepted_change_count: 0,
|
||||
rejected_change_count: 0,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt
|
||||
},
|
||||
changes: [],
|
||||
commit_attempts: []
|
||||
})
|
||||
|
||||
return { reviewId }
|
||||
}
|
||||
@@ -4,18 +4,18 @@ import type {
|
||||
AiRunContext
|
||||
} from '../../../src/main/services/ai/types'
|
||||
import type { HumanTaskRunner } from '../../../src/main/services/ai/types'
|
||||
import type { DraftReviewService } from '../../../src/main/services/ai/human/tools/docx/review/DraftReviewService'
|
||||
import type { FileReviewService } from '../../../src/main/services/ai/human/tools/docx/review/FileReviewService'
|
||||
import type { ChatStore } from '../../../src/main/services/storage/domains/ChatStore'
|
||||
import type { DocumentsStore } from '../../../src/main/services/storage/domains/DocumentsStore'
|
||||
import type { DraftReviewStore } from '../../../src/main/services/storage/domains/DraftReviewStore'
|
||||
import type { FileReviewStore } from '../../../src/main/services/storage/domains/FileReviewStore'
|
||||
import type { LlmConfigStore } from '../../../src/main/services/storage/domains/LlmConfigStore'
|
||||
import type { StorageService } from '../../../src/main/services/storage/StorageService'
|
||||
import type { EmbeddingService } from '../../../src/main/services/vector/EmbeddingService'
|
||||
import type {
|
||||
ChatMessage,
|
||||
ChatSessionMemory,
|
||||
DraftReviewDocument,
|
||||
DraftReviewStatus,
|
||||
FileReview,
|
||||
FileReviewStatus,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
LLMConfig,
|
||||
@@ -122,12 +122,12 @@ export type SeedChatHistoryInput = {
|
||||
memory?: ChatSessionMemory
|
||||
}
|
||||
|
||||
export type SeedDraftReviewInput = {
|
||||
export type SeedFileReviewInput = {
|
||||
reviewId?: string
|
||||
sessionId?: string
|
||||
runId?: string
|
||||
title?: string
|
||||
status?: DraftReviewStatus
|
||||
status?: FileReviewStatus
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
documents: readonly Array<{
|
||||
@@ -135,7 +135,7 @@ export type SeedDraftReviewInput = {
|
||||
confirmBasePath?: string
|
||||
sourceName?: string
|
||||
sourceDocumentId?: string
|
||||
format?: DraftReviewDocument['format']
|
||||
format?: FileReview['format']
|
||||
latestDraftSnapshotId?: string
|
||||
}>
|
||||
}
|
||||
@@ -165,10 +165,10 @@ export type BackendTestRuntime = {
|
||||
storageService: StorageService
|
||||
documentsStore: DocumentsStore
|
||||
chatStore: ChatStore
|
||||
draftReviewStore: DraftReviewStore
|
||||
fileReviewStore: FileReviewStore
|
||||
llmStore: LlmConfigStore
|
||||
embeddingService: EmbeddingService
|
||||
draftReviewService: DraftReviewService
|
||||
fileReviewService: FileReviewService
|
||||
recordEvent: (input: Omit<TestLogEvent, 'ts'>) => void
|
||||
assertThat: (condition: boolean, input: { name: string; message: string; data?: JsonValue }) => void
|
||||
getFixtureWorkspacePath: (sourcePath: string) => string
|
||||
@@ -179,7 +179,7 @@ export type BackendTestRuntime = {
|
||||
ensureEmbeddingReady: () => Promise<void>
|
||||
seedKb: (input: readonly SeedKbDocumentInput[]) => Promise<void>
|
||||
seedChatHistory: (input: SeedChatHistoryInput) => Promise<void>
|
||||
seedDraftReview: (input: SeedDraftReviewInput) => Promise<{ reviewId: string }>
|
||||
seedFileReview: (input: SeedFileReviewInput) => Promise<{ reviewId: string }>
|
||||
seedLlmConfig: (config: LiveLlmConfig) => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
@@ -75,17 +75,15 @@ describe('ai unit mouth readyToDeliver guard', () => {
|
||||
})
|
||||
}).system
|
||||
|
||||
expect(prompt).toContain('<available_information>')
|
||||
expect(prompt).toContain('<context_information>')
|
||||
expect(prompt).toContain('<output_contract>')
|
||||
expect(prompt).toContain('吸收当前任务在完整上下文下已经成立的信息')
|
||||
expect(prompt).toContain('把用户诉求、当前事实、已有结果与可引用产物自然组织成回复')
|
||||
expect(prompt).toContain('面向用户回答 task_goal')
|
||||
expect(prompt).toContain('回答应基于当前整体状态自然组织')
|
||||
expect(prompt).toContain('优先依据 your_notes 组织答案或交付内容')
|
||||
expect(prompt).toContain('与更具体的证据记录存在张力')
|
||||
expect(prompt).toContain('reference_scope')
|
||||
expect(prompt).toContain('当前工作区、默认资料范围、显式选择的优先参考资料与当前缩圈状态')
|
||||
expect(prompt).toContain('回答面向用户,但内容必须忠实于当前整体状态')
|
||||
expect(prompt).toContain('优先吸收 reference_scope、your_notes、history、runtime_artifacts')
|
||||
expect(prompt).toContain('只输出最终 markdown 正文')
|
||||
expect(prompt).toContain('如果最终答案补充了不来自当前本地资料的信息')
|
||||
expect(prompt).not.toContain('必须在结尾单独加一行“补充说明')
|
||||
expect(prompt).toContain('如果需要回复不在上下文中的内容时')
|
||||
expect(prompt).toContain('补充说明')
|
||||
})
|
||||
|
||||
it('keeps mouth user-context blocks data-only', () => {
|
||||
@@ -96,9 +94,10 @@ describe('ai unit mouth readyToDeliver guard', () => {
|
||||
})
|
||||
})
|
||||
|
||||
expect(result.system).toContain('task_goal:用户最初要你完成什么')
|
||||
expect(result.system).toContain('当前理解:当前任务的最新长期理解')
|
||||
expect(result.system).toContain('task_goal:用户的诉求')
|
||||
expect(result.system).toContain('your_notes:当前持续工作的任务笔记')
|
||||
expect(result.system).not.toContain('final_decision')
|
||||
expect(result.user).toContain('<reference_scope>')
|
||||
expect(result.user).not.toContain('这是当前交付模式。')
|
||||
expect(result.user).not.toContain('这是用户这次原始诉求与限制。')
|
||||
expect(result.user).toContain('<history>')
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createInitialHumanState } from '../../../src/main/services/ai/human/state/schema'
|
||||
import { renderHumanArtifactLines } from '../../../src/main/services/ai/human/shared/runtimeRenderers'
|
||||
|
||||
describe('ai unit runtime artifacts renderer', () => {
|
||||
it('renders changed files in prompt mode', () => {
|
||||
const state = createInitialHumanState({
|
||||
taskId: 'task-runtime-1',
|
||||
userInput: '整理文档'
|
||||
})
|
||||
state.execution.runtimeArtifacts = {
|
||||
changed_files: [
|
||||
{
|
||||
file_name: 'sample.docx',
|
||||
source_path: '/workspace/sample.docx',
|
||||
format: 'docx',
|
||||
change_count: 2,
|
||||
summary: '补充了课堂说明',
|
||||
change_highlights: ['补充交互设计说明', '新增总结表'],
|
||||
draft_artifact_path: '/workspace/.ncurator/file-reviews/review-1/draft-sample.docx'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const rendered = renderHumanArtifactLines(state, 'prompt')
|
||||
|
||||
expect(rendered).toContain('文件:sample.docx')
|
||||
expect(rendered).toContain('改动数:2')
|
||||
expect(rendered).toContain('补充交互设计说明')
|
||||
expect(rendered).toContain('待确认稿:/workspace/.ncurator/file-reviews/review-1/draft-sample.docx')
|
||||
})
|
||||
|
||||
it('renders empty fallback when no changed files exist', () => {
|
||||
const state = createInitialHumanState({
|
||||
taskId: 'task-runtime-2',
|
||||
userInput: '整理文档'
|
||||
})
|
||||
|
||||
const rendered = renderHumanArtifactLines(state, 'prompt')
|
||||
|
||||
expect(rendered).toContain('本次运行未形成新的文件改动结果。')
|
||||
expect(rendered).not.toContain('draft review')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user