feat: enhance docx handling with style text support and review highlights
This commit is contained in:
1 parent
7815a9fb21
commit
1e2009387d
8 files changed
+452
-16
No files matched your search
@@ -13,6 +13,7 @@ export const buildMouthPrompt = (input: { state: HumanState; ctx?: AiRunContext
|
||||
- 你的职责是根据context_information的信息,面向用户交付最终结果,而不是复述系统内部过程。
|
||||
- 优先回答用户真正关心的内容,优先使用更自然、更容易理解的表达。
|
||||
- your_notes、history、delivery_guidance 和 runtime_artifacts 主要用于核对事实和补充细节,不要求按原样转述。
|
||||
- 严格按照当前上下文信息回答,不得编造、补写或改写为上下文中未明确给出的已执行结果。
|
||||
- 如果提供了额外的交付说明,优先将其转化为用户容易理解的最终答复,并确保相关状态被明确说明。
|
||||
- 回答必须忠实于当前已经成立的信息;信息不足时直接说明,不补写未确认的结果。
|
||||
- 如果需要使用不来自当前上下文的通用知识,在结尾单独写“补充说明:...”,明确说明来源不是当前本地资料。
|
||||
|
||||
@@ -340,7 +340,58 @@ const summarizeArrayTexts = (value: JsonValue | undefined, key: string): string[
|
||||
.filter((item): item is string => Boolean(item && item.length > 0))
|
||||
}
|
||||
|
||||
const summarizeToolResultForMouth = (result: HumanHistoryToolResult): string => {
|
||||
const MOUTH_DOCX_TEXT_LIMIT = 420
|
||||
const MOUTH_DOCX_TABLE_MAX_ROWS = 4
|
||||
|
||||
const clipText = (value: string, maxChars: number): string => {
|
||||
const normalized = value.replace(/\s+/g, ' ').trim()
|
||||
if (!normalized) return ''
|
||||
if (normalized.length <= maxChars) return normalized
|
||||
return `${normalized.slice(0, maxChars - 1).trim()}…`
|
||||
}
|
||||
|
||||
const renderMouthToolArgValue = (value: JsonValue | undefined): string => {
|
||||
if (value === undefined) return 'none'
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
|
||||
const buildDocxWriteFact = (input: {
|
||||
toolName: HumanHistoryToolResult['toolName']
|
||||
action?: string
|
||||
target?: string
|
||||
contentLabel: string
|
||||
content: JsonValue | undefined
|
||||
extraLines?: string[]
|
||||
}): string => {
|
||||
const header = [`\`${input.toolName}\` 已完成实际写入。`]
|
||||
if (input.action) header.push(`action=${input.action}`)
|
||||
if (input.target) header.push(`target=${input.target}`)
|
||||
const contentText =
|
||||
typeof input.content === 'string'
|
||||
? clipText(input.content, MOUTH_DOCX_TEXT_LIMIT)
|
||||
: renderMouthToolArgValue(input.content)
|
||||
return [
|
||||
header.join(';'),
|
||||
` - ${input.contentLabel}:`,
|
||||
contentText
|
||||
.split('\n')
|
||||
.map((line) => ` ${line}`)
|
||||
.join('\n'),
|
||||
...(input.extraLines || []).map((line) => ` - ${line}`)
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const buildTableRowsPreview = (rows: JsonValue | undefined): JsonValue | undefined => {
|
||||
if (!Array.isArray(rows)) return rows
|
||||
if (rows.length <= MOUTH_DOCX_TABLE_MAX_ROWS) return rows
|
||||
return [...rows.slice(0, MOUTH_DOCX_TABLE_MAX_ROWS), `... 其余 ${String(rows.length - MOUTH_DOCX_TABLE_MAX_ROWS)} 行省略`]
|
||||
}
|
||||
|
||||
const summarizeToolResultForMouth = (input: {
|
||||
result: HumanHistoryToolResult
|
||||
call?: ToolCallParam
|
||||
}): string => {
|
||||
const { result, call } = input
|
||||
if (result.diagnosticText) {
|
||||
return `\`${result.toolName}\` 未完成:${result.diagnosticText}`
|
||||
}
|
||||
@@ -371,17 +422,67 @@ const summarizeToolResultForMouth = (result: HumanHistoryToolResult): string =>
|
||||
? `\`${result.toolName}\` 概览到 ${String(count)} 个块:${blocks.join(' / ')}`
|
||||
: `\`${result.toolName}\` 概览到 ${String(count)} 个块。`
|
||||
}
|
||||
case 'docx_text_edit':
|
||||
case 'docx_paragraph_edit':
|
||||
case 'docx_heading_edit':
|
||||
case 'docx_table_edit':
|
||||
case 'docx_heading_edit': {
|
||||
return buildDocxWriteFact({
|
||||
toolName: result.toolName,
|
||||
action: typeof call?.args.action === 'string' ? call.args.action : undefined,
|
||||
target:
|
||||
typeof call?.args.blockId === 'string'
|
||||
? `blockId=${call.args.blockId}`
|
||||
: typeof call?.args.anchorBlockId === 'string'
|
||||
? `anchorBlockId=${call.args.anchorBlockId}`
|
||||
: undefined,
|
||||
contentLabel: '实际写入文本',
|
||||
content: typeof call?.args.text === 'string' ? call.args.text : undefined
|
||||
})
|
||||
}
|
||||
case 'docx_table_edit': {
|
||||
const rawRows = Array.isArray(call?.args.rows) ? call.args.rows : undefined
|
||||
const rows = buildTableRowsPreview(rawRows)
|
||||
return buildDocxWriteFact({
|
||||
toolName: result.toolName,
|
||||
action: typeof call?.args.action === 'string' ? call.args.action : undefined,
|
||||
target:
|
||||
typeof call?.args.blockId === 'string'
|
||||
? `blockId=${call.args.blockId}`
|
||||
: typeof call?.args.anchorBlockId === 'string'
|
||||
? `anchorBlockId=${call.args.anchorBlockId}`
|
||||
: undefined,
|
||||
contentLabel: '实际写入 rows',
|
||||
content: rows,
|
||||
extraLines: Array.isArray(rawRows) && rawRows.length > MOUTH_DOCX_TABLE_MAX_ROWS
|
||||
? [`原始表格总行数=${String(rawRows.length)}`]
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
case 'docx_text_edit': {
|
||||
return buildDocxWriteFact({
|
||||
toolName: result.toolName,
|
||||
target:
|
||||
Array.isArray(call?.args.blockIds) && call.args.blockIds.length > 0
|
||||
? `blockIds=${renderMouthToolArgValue(call.args.blockIds)}`
|
||||
: undefined,
|
||||
contentLabel: '实际替换参数',
|
||||
content: {
|
||||
query: typeof call?.args.query === 'string' ? call.args.query : undefined,
|
||||
replace: typeof call?.args.replace === 'string' ? call.args.replace : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
case 'docx_text_style_edit': {
|
||||
const summary = readStringField(result.data, 'summary')
|
||||
const changedBlocks = Array.isArray(result.data.changedBlocks) ? result.data.changedBlocks.length : 0
|
||||
if (summary) return `\`${result.toolName}\`:${summary}`
|
||||
return changedBlocks > 0
|
||||
? `\`${result.toolName}\` 已形成 ${String(changedBlocks)} 处修改。`
|
||||
: `\`${result.toolName}\` 已完成。`
|
||||
return buildDocxWriteFact({
|
||||
toolName: result.toolName,
|
||||
target:
|
||||
typeof call?.args.blockId === 'string'
|
||||
? `blockId=${call.args.blockId}`
|
||||
: undefined,
|
||||
contentLabel: '实际样式参数',
|
||||
content: {
|
||||
query: typeof call?.args.query === 'string' ? call.args.query : undefined,
|
||||
style: isJsonObject(call?.args.style) ? call.args.style : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
default: {
|
||||
const summary =
|
||||
@@ -410,7 +511,12 @@ const renderMouthRoundFacts = (record: HumanRoundRecord): string[] => {
|
||||
return facts
|
||||
}
|
||||
case 'hand': {
|
||||
const facts = record.execution.toolResults.map(summarizeToolResultForMouth)
|
||||
const facts = record.execution.toolResults.map((result, index) =>
|
||||
summarizeToolResultForMouth({
|
||||
result,
|
||||
call: record.execution.toolUsePlan.calls[index]
|
||||
})
|
||||
)
|
||||
if (record.execution.watchHint) {
|
||||
facts.push(`收敛提示:${summarizeWatchHint(record.execution.watchHint)}`)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ const buildNode = (input: {
|
||||
spec: DocxBlockSpec
|
||||
createdRevisionRef: string
|
||||
changedRevisionRef?: string
|
||||
reviewQuery?: string
|
||||
}): DocxReplayNode => ({
|
||||
nodeId: `${input.reviewState}:${input.blockId}:${input.createdRevisionRef}:${input.changedRevisionRef || 'active'}`,
|
||||
blockId: input.blockId,
|
||||
@@ -110,7 +111,8 @@ const buildNode = (input: {
|
||||
currentBlockId: input.currentBlockId,
|
||||
spec: input.spec,
|
||||
createdRevisionRef: input.createdRevisionRef,
|
||||
changedRevisionRef: input.changedRevisionRef
|
||||
changedRevisionRef: input.changedRevisionRef,
|
||||
reviewQuery: input.reviewQuery
|
||||
})
|
||||
|
||||
const reindexBodyNodes = (session: DocxReplaySession): void => {
|
||||
@@ -255,7 +257,8 @@ const cloneNodeAsOldRevision = (
|
||||
nodeId: `${node.nodeId}:${reviewState}:${revisionRef}`,
|
||||
currentBlockId: undefined,
|
||||
reviewState,
|
||||
changedRevisionRef: revisionRef
|
||||
changedRevisionRef: revisionRef,
|
||||
reviewQuery: undefined
|
||||
})
|
||||
|
||||
const buildCurrentNodeFromSnapshot = (
|
||||
@@ -427,10 +430,34 @@ export const applyRevisionToReplaySession = (input: {
|
||||
return
|
||||
}
|
||||
|
||||
if (op.type === 'style_text') {
|
||||
const snapshotBlock = resolveSnapshotBlockForMutation({
|
||||
session,
|
||||
nextSnapshot,
|
||||
createdBlocks,
|
||||
targetBlockId
|
||||
})
|
||||
if (!snapshotBlock) {
|
||||
targetNode.reviewState = 'formatted'
|
||||
targetNode.changedRevisionRef = revisionRef
|
||||
targetNode.reviewQuery = op.query
|
||||
session.activeSnapshot = nextSnapshot
|
||||
return
|
||||
}
|
||||
|
||||
targetNode.reviewState = 'formatted'
|
||||
targetNode.currentBlockId = snapshotBlock.blockId
|
||||
targetNode.bodyIndex = snapshotBlock.bodyIndex
|
||||
targetNode.spec = snapshotBlock.spec
|
||||
targetNode.changedRevisionRef = revisionRef
|
||||
targetNode.reviewQuery = op.query
|
||||
session.activeSnapshot = nextSnapshot
|
||||
return
|
||||
}
|
||||
|
||||
const oldState =
|
||||
op.type === 'replace_block' ||
|
||||
op.type === 'replace_text' ||
|
||||
op.type === 'style_text' ||
|
||||
op.type === 'update_table_cell'
|
||||
? 'replaced-old'
|
||||
: 'deleted'
|
||||
@@ -478,6 +505,7 @@ export const projectReplayNodes = (
|
||||
if (kind === 'review') return true
|
||||
return (
|
||||
node.reviewState === 'normal' ||
|
||||
node.reviewState === 'formatted' ||
|
||||
node.reviewState === 'inserted' ||
|
||||
node.reviewState === 'replaced-new'
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import { projectCommittedView, projectReviewView } from './viewProjector'
|
||||
|
||||
const WORD_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||
|
||||
type RevisionKind = 'inserted' | 'deleted'
|
||||
type RevisionKind = 'inserted' | 'deleted' | 'formatted'
|
||||
|
||||
const ensureDirForFile = async (filePath: string): Promise<void> => {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
@@ -56,6 +56,14 @@ const styleRunForRevision = (run: Element, kind: RevisionKind): void => {
|
||||
return
|
||||
}
|
||||
|
||||
if (kind === 'formatted') {
|
||||
const shade = firstChildElement(rPr, 'shd') || run.ownerDocument.createElementNS(WORD_NS, 'w:shd')
|
||||
setWordAttribute(shade, 'val', 'clear')
|
||||
setWordAttribute(shade, 'fill', 'CFFAFE')
|
||||
if (!shade.parentNode) rPr.appendChild(shade)
|
||||
return
|
||||
}
|
||||
|
||||
const color = run.ownerDocument.createElementNS(WORD_NS, 'w:color')
|
||||
const shade = run.ownerDocument.createElementNS(WORD_NS, 'w:shd')
|
||||
setWordAttribute(color, 'val', '166534')
|
||||
@@ -79,6 +87,48 @@ const applyRevisionToRuns = (element: Element, kind: RevisionKind): void => {
|
||||
})
|
||||
}
|
||||
|
||||
const getRunText = (run: Element): string =>
|
||||
Array.from({ length: run.childNodes.length })
|
||||
.map((_, index) => run.childNodes.item(index))
|
||||
.filter(Boolean)
|
||||
.map((child) => {
|
||||
if (!isElementNode(child)) return child.textContent || ''
|
||||
if (child.localName === 't') return child.textContent || ''
|
||||
return getRunText(child)
|
||||
})
|
||||
.join('')
|
||||
|
||||
const collectRunElements = (element: Element): Element[] => {
|
||||
const result: Element[] = []
|
||||
const stack: Element[] = [element]
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()
|
||||
if (!current) continue
|
||||
if (current.localName === 'r') {
|
||||
result.push(current)
|
||||
continue
|
||||
}
|
||||
Array.from({ length: current.childNodes.length })
|
||||
.map((_, index) => current.childNodes.item(index))
|
||||
.filter(isElementNode)
|
||||
.forEach((child) => stack.push(child))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const applyFormattedMarker = (element: Element, query?: string): void => {
|
||||
const normalizedQuery = String(query || '').trim()
|
||||
const runs = collectRunElements(element)
|
||||
if (runs.length === 0) return
|
||||
|
||||
const matchedRuns =
|
||||
normalizedQuery.length > 0
|
||||
? runs.filter((run) => getRunText(run).includes(normalizedQuery))
|
||||
: runs
|
||||
const targetRuns = matchedRuns.length > 0 ? matchedRuns : runs
|
||||
targetRuns.forEach((run) => styleRunForRevision(run, 'formatted'))
|
||||
}
|
||||
|
||||
const createTextRun = (
|
||||
doc: Document,
|
||||
text: string,
|
||||
@@ -293,6 +343,10 @@ const resolveElementForNode = (input: {
|
||||
}
|
||||
|
||||
const applyReviewMarker = (element: Element, node: DocxReplayNode): void => {
|
||||
if (node.reviewState === 'formatted') {
|
||||
applyFormattedMarker(element, node.reviewQuery)
|
||||
return
|
||||
}
|
||||
if (node.reviewState === 'inserted' || node.reviewState === 'replaced-new') {
|
||||
applyRevisionToRuns(element, 'inserted')
|
||||
return
|
||||
|
||||
@@ -45,6 +45,10 @@ export const getDocxChangeKind = (op: DocxOp): FileReviewChange['change_kind'] =
|
||||
op.type === 'update_table_cell' ? 'table_cell' : 'block'
|
||||
|
||||
export const buildDocxChangeTitle = (op: DocxOp, change: DocxChangedBlock): string => {
|
||||
if (op.type === 'style_text') {
|
||||
const query = String(op.query || '').trim()
|
||||
return query ? `更新文本样式: ${query}` : '更新文本样式'
|
||||
}
|
||||
const baseText = String(change.afterText || change.beforeText).trim()
|
||||
const previewRaw = clipText(baseText, 28).trim()
|
||||
const preview = previewRaw.length < baseText.length ? `${previewRaw}...` : previewRaw
|
||||
@@ -52,7 +56,9 @@ export const buildDocxChangeTitle = (op: DocxOp, change: DocxChangedBlock): stri
|
||||
}
|
||||
|
||||
export const buildDocxChangeSummary = (op: DocxOp, change: DocxChangedBlock): string =>
|
||||
`${describeDocxOp(op)} ${change.blockId}`
|
||||
op.type === 'style_text'
|
||||
? `${describeDocxOp(op)} ${change.blockId}${String(op.query || '').trim() ? `: ${String(op.query).trim()}` : ''}`
|
||||
: `${describeDocxOp(op)} ${change.blockId}`
|
||||
|
||||
export const buildDocxChangeLocator = (change: DocxChangedBlock): JsonObject => ({
|
||||
blockId: change.blockId,
|
||||
|
||||
@@ -267,6 +267,7 @@ export type DocxReplaySession = {
|
||||
|
||||
export type DocxReviewState =
|
||||
| 'normal'
|
||||
| 'formatted'
|
||||
| 'inserted'
|
||||
| 'deleted'
|
||||
| 'replaced-old'
|
||||
@@ -285,6 +286,7 @@ export type DocxReplayNode = {
|
||||
spec: DocxBlockSpec
|
||||
createdRevisionRef: string
|
||||
changedRevisionRef?: string
|
||||
reviewQuery?: string
|
||||
}
|
||||
|
||||
export type DocxViewArtifact = {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import { XMLSerializer } from '@xmldom/xmldom'
|
||||
import { describe, it } from 'vitest'
|
||||
import { loadDocxBundle } from '../../../../src/main/services/ai/human/tools/docx/adapters/ooxml/blockIndex'
|
||||
import {
|
||||
createDocxToolHarness,
|
||||
writeRichTestDocxFile
|
||||
} from '../../../harness/backend/aiToolHarness'
|
||||
import { runBackendCase } from '../../../harness/backend/run-backend-case'
|
||||
import type { DocxMutationResult, DocxOutlineResult } from './testUtils'
|
||||
|
||||
describe('ai tool docx backend', () => {
|
||||
it('docx-style-text-review-highlight', async () => {
|
||||
await runBackendCase({
|
||||
caseId: 'docx-style-text-review-highlight',
|
||||
harnessMode: 'tool-real',
|
||||
domain: 'ai',
|
||||
suite: 'tool',
|
||||
groups: ['docx'],
|
||||
description: '验证 style_text 会在 review artifact 中以独立浅青色高亮显示,并且 committed artifact 不保留该高亮。',
|
||||
fixtures: [],
|
||||
runMode: 'single',
|
||||
execute: async (runtime) => {
|
||||
await writeRichTestDocxFile(runtime.workspaceRoot, 'style-highlight.docx', [
|
||||
{
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
runs: [{ text: '负责人:Alice 将在周五完成复核。' }]
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
runtime.fileReviewService.beginRun({
|
||||
runId: runtime.runId,
|
||||
sessionId: 'docx-style-highlight-session',
|
||||
inputText: '把 Alice 标成重点'
|
||||
})
|
||||
|
||||
const harness = createDocxToolHarness(runtime)
|
||||
try {
|
||||
const outline = await harness.invoke<DocxOutlineResult>('docx_outline', {
|
||||
sourcePath: 'style-highlight.docx'
|
||||
})
|
||||
const paragraphBlockId =
|
||||
outline.blocks.find(
|
||||
(block) => block.type === 'paragraph' && block.preview.includes('Alice')
|
||||
)?.blockId || ''
|
||||
const styled = await harness.invoke<DocxMutationResult>('docx_text_style_edit', {
|
||||
sourcePath: 'style-highlight.docx',
|
||||
blockId: paragraphBlockId,
|
||||
query: 'Alice',
|
||||
style: {
|
||||
bold: true,
|
||||
color: '2563EB'
|
||||
}
|
||||
})
|
||||
const details = await runtime.fileReviewService.finalizeRunReview({
|
||||
runId: runtime.runId,
|
||||
workspaceRootPath: runtime.workspaceRoot
|
||||
})
|
||||
const detail = details[0]
|
||||
if (
|
||||
!detail ||
|
||||
!detail.review.draft_artifact_path ||
|
||||
!detail.review.committed_artifact_path
|
||||
) {
|
||||
throw new Error('expected generated review and committed artifacts')
|
||||
}
|
||||
|
||||
const reviewBuffer = await fs.readFile(detail.review.draft_artifact_path)
|
||||
const committedBuffer = await fs.readFile(detail.review.committed_artifact_path)
|
||||
const reviewBundle = await loadDocxBundle(reviewBuffer)
|
||||
const committedBundle = await loadDocxBundle(committedBuffer)
|
||||
|
||||
return {
|
||||
styled,
|
||||
reviewPath: detail.review.draft_artifact_path,
|
||||
committedPath: detail.review.committed_artifact_path,
|
||||
reviewXml: new XMLSerializer().serializeToString(reviewBundle.documentDom),
|
||||
committedXml: new XMLSerializer().serializeToString(committedBundle.documentDom),
|
||||
changeTitles: detail.changes.map((change) => change.title),
|
||||
changeSummaries: detail.changes.map((change) => change.summary)
|
||||
}
|
||||
} finally {
|
||||
await harness.release()
|
||||
}
|
||||
},
|
||||
assert: async (result, runtime) => {
|
||||
runtime.assertThat(
|
||||
result.styled.changedBlocks.some((block) => block.action === 'style_text') &&
|
||||
result.changeTitles.includes('更新文本样式: Alice') &&
|
||||
result.changeSummaries.some((summary) => summary.includes('更新文本样式') && summary.includes('Alice')),
|
||||
{
|
||||
name: 'style-text-review-summary-uses-style-specific-copy',
|
||||
message: 'style_text 进入 review 记录后,应使用独立的样式修改标题和摘要。',
|
||||
data: {
|
||||
changedBlocks: result.styled.changedBlocks,
|
||||
changeTitles: result.changeTitles,
|
||||
changeSummaries: result.changeSummaries
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
runtime.assertThat(
|
||||
result.reviewXml.includes('CFFAFE') && !result.committedXml.includes('CFFAFE'),
|
||||
{
|
||||
name: 'style-text-review-highlight-stays-review-only',
|
||||
message: 'style_text 应只在 review artifact 中带浅青色高亮,committed artifact 不应保留该显示层标记。',
|
||||
data: {
|
||||
reviewPath: result.reviewPath,
|
||||
committedPath: result.committedPath,
|
||||
reviewHasHighlight: result.reviewXml.includes('CFFAFE'),
|
||||
committedHasHighlight: result.committedXml.includes('CFFAFE'),
|
||||
reviewXmlSnippet: result.reviewXml.slice(0, 1200),
|
||||
committedXmlSnippet: result.committedXml.slice(0, 1200)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -119,6 +119,7 @@ describe('ai unit mouth checklist guard', () => {
|
||||
expect(prompt).toContain('只输出最终 markdown 正文')
|
||||
expect(prompt).toContain('如果需要使用不来自当前上下文的通用知识')
|
||||
expect(prompt).toContain('补充说明')
|
||||
expect(prompt).toContain('严格按照当前上下文信息回答,不得编造')
|
||||
})
|
||||
|
||||
it('keeps mouth user-context blocks data-only', () => {
|
||||
@@ -324,6 +325,122 @@ describe('ai unit mouth checklist guard', () => {
|
||||
expect(prompt.user).not.toContain('fromBlockId')
|
||||
})
|
||||
|
||||
it('renders actual docx write args in mouth history for paragraph and table edits', () => {
|
||||
const initialState = createInitialHumanState({
|
||||
taskId: 'task-6',
|
||||
userInput: '把主稿补充完整并插入表格'
|
||||
})
|
||||
const history = appendRoundRecord({
|
||||
state: initialState,
|
||||
decision: {
|
||||
decision: {
|
||||
summary: '写入主稿段落和表格',
|
||||
reason: '当前已经明确知道要写入的正文和表格内容',
|
||||
expectedOutcome: '让 mouth 严格依据真实写入内容回答'
|
||||
},
|
||||
nextAction: {
|
||||
action: 'hand',
|
||||
toolUsePlan: {
|
||||
mode: 'serial',
|
||||
calls: [
|
||||
{
|
||||
toolName: 'docx_paragraph_edit',
|
||||
args: {
|
||||
sourcePath: 'prd.docx',
|
||||
action: 'replace_text',
|
||||
blockId: 'b:31',
|
||||
text: '每个探究环节应包含四个核心设计要素:探究目标、探究素材、学生活动设计、环节推进条件。'
|
||||
}
|
||||
},
|
||||
{
|
||||
toolName: 'docx_table_edit',
|
||||
args: {
|
||||
sourcePath: 'prd.docx',
|
||||
action: 'insert_after',
|
||||
anchorBlockId: 'b:32',
|
||||
rows: [
|
||||
['设计要素', '关键内容', '互动形式示例'],
|
||||
['探究目标', '明确学生需达成的认知目标', '情境引入、问题聚焦、概念生成'],
|
||||
['探究素材', '提供视频、图片、数据等支撑材料', '耐克迁移视频、生产地图、成本数据表']
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
execution: buildHandExecution({
|
||||
toolUsePlan: {
|
||||
mode: 'serial',
|
||||
calls: [
|
||||
{
|
||||
toolName: 'docx_paragraph_edit',
|
||||
args: {
|
||||
sourcePath: 'prd.docx',
|
||||
action: 'replace_text',
|
||||
blockId: 'b:31',
|
||||
text: '每个探究环节应包含四个核心设计要素:探究目标、探究素材、学生活动设计、环节推进条件。'
|
||||
}
|
||||
},
|
||||
{
|
||||
toolName: 'docx_table_edit',
|
||||
args: {
|
||||
sourcePath: 'prd.docx',
|
||||
action: 'insert_after',
|
||||
anchorBlockId: 'b:32',
|
||||
rows: [
|
||||
['设计要素', '关键内容', '互动形式示例'],
|
||||
['探究目标', '明确学生需达成的认知目标', '情境引入、问题聚焦、概念生成'],
|
||||
['探究素材', '提供视频、图片、数据等支撑材料', '耐克迁移视频、生产地图、成本数据表']
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
toolResults: [
|
||||
{
|
||||
toolName: 'docx_paragraph_edit',
|
||||
ok: true,
|
||||
code: 'ok',
|
||||
data: {
|
||||
ok: true,
|
||||
code: 'ok',
|
||||
summary: '段落replace_text 完成,共 1 处变更'
|
||||
}
|
||||
},
|
||||
{
|
||||
toolName: 'docx_table_edit',
|
||||
ok: true,
|
||||
code: 'ok',
|
||||
data: {
|
||||
ok: true,
|
||||
code: 'ok',
|
||||
summary: '表格insert_after 完成,共 1 处变更'
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
createdAt: 3
|
||||
})
|
||||
|
||||
const prompt = buildMouthPrompt({
|
||||
state: {
|
||||
...initialState,
|
||||
memory: {
|
||||
...initialState.memory,
|
||||
history
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(prompt.user).toContain('`docx_paragraph_edit` 已完成实际写入。')
|
||||
expect(prompt.user).toContain('每个探究环节应包含四个核心设计要素')
|
||||
expect(prompt.user).toContain('`docx_table_edit` 已完成实际写入。')
|
||||
expect(prompt.user).toContain('设计要素')
|
||||
expect(prompt.user).toContain('互动形式示例')
|
||||
expect(prompt.user).toContain('耐克迁移视频')
|
||||
})
|
||||
|
||||
it('renders delivery guidance when present', () => {
|
||||
const initialState = createInitialHumanState({
|
||||
taskId: 'task-5',
|
||||
|
||||
Reference in new issue
Block a user