Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ private void runLoop(StreamingChatLanguageModel model,
});

// 编辑器实时流式写入拦截(事件名沿用 wps_stream_data,前后端契约)
handler.setOnWpsStream(token -> {
handler.setOnEditorStream(token -> {
if (editorBridgeService.isStreamingMode(conversationId)) {
sseEmitterService.send(conversationId, "wps_stream_data", java.util.Map.of("content", token));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ public AgentStreamHandler(SseEmitterService sseEmitterService, String conversati

// Callback for each token generated (for real-time tracking)
private java.util.function.Consumer<String> onToken;
private java.util.function.Consumer<String> onWpsStream;
private java.util.function.Consumer<String> onEditorStream;

public void setOnToken(java.util.function.Consumer<String> onToken) {
this.onToken = onToken;
}

public void setOnWpsStream(java.util.function.Consumer<String> onWpsStream) {
this.onWpsStream = onWpsStream;
public void setOnEditorStream(java.util.function.Consumer<String> onEditorStream) {
this.onEditorStream = onEditorStream;
}

@Override
Expand All @@ -59,18 +59,18 @@ public void onNext(String token) {
onToken.accept(token);
}

// Process for WPS filtered stream
processWpsStream(token);
// Process for editor filtered stream
processEditorStream(token);

processBuffer(token);
}
}

// ==================== WPS Stream Filtering Logic ====================
// ==================== Editor Stream Filtering Logic(过滤后实时写入编辑器文档;SSE 事件名 wps_stream_data 为前后端契约保留) ====================

// Buffer for WPS stream parser to handle split tags
private final StringBuilder wpsBuffer = new StringBuilder();
// Set of tags that should be hidden from WPS (but content might be hidden too?)
// Buffer for editor stream parser to handle split tags
private final StringBuilder editorStreamBuffer = new StringBuilder();
// Set of tags that should be hidden from the editor stream (but content might be hidden too?)
// Protocol:
// <thinking>...</thinking> -> Hide ALL
// <process>...</process> -> Hide ALL
Expand All @@ -96,54 +96,54 @@ public void onNext(String token) {
"bubble_type" // Also hide bubble control tags
);

private void processWpsStream(String token) {
if (onWpsStream == null) return;
private void processEditorStream(String token) {
if (onEditorStream == null) return;

wpsBuffer.append(token);
editorStreamBuffer.append(token);

while (wpsBuffer.length() > 0) {
while (editorStreamBuffer.length() > 0) {
// If we are NOT inside a hidden tag, we look for start of ANY tag
if (!isInsideHiddenTag) {
int ltIndex = wpsBuffer.indexOf("<");
int ltIndex = editorStreamBuffer.indexOf("<");
if (ltIndex == -1) {
// No tags in buffer, safe to emit all
String text = wpsBuffer.toString();
emitWpsText(text);
wpsBuffer.setLength(0);
String text = editorStreamBuffer.toString();
emitEditorText(text);
editorStreamBuffer.setLength(0);
return;
} else {
// Valid text before the tag
if (ltIndex > 0) {
emitWpsText(wpsBuffer.substring(0, ltIndex));
wpsBuffer.delete(0, ltIndex);
emitEditorText(editorStreamBuffer.substring(0, ltIndex));
editorStreamBuffer.delete(0, ltIndex);
// Now buffer starts with '<'
}

// Check if we have enough chars to identify the tag
// Need at least "<x" or "</x"
if (wpsBuffer.length() < 2) {
if (editorStreamBuffer.length() < 2) {
return; // Wait for more data
}

// Determine if it's a start tag or end tag
boolean isEndTag = wpsBuffer.charAt(1) == '/';
boolean isEndTag = editorStreamBuffer.charAt(1) == '/';

// Try to find the closing '>'
int gtIndex = wpsBuffer.indexOf(">");
int gtIndex = editorStreamBuffer.indexOf(">");
if (gtIndex == -1) {
// Tag not fully received yet
// Safety cap: if buffer gets too huge without '>', force flush?
if (wpsBuffer.length() > 1000) {
if (editorStreamBuffer.length() > 1000) {
// Something wrong, just flush to avoid memory issues, though it might break protocol.
// But for WPS stream, better to show garbage than crash.
emitWpsText(wpsBuffer.toString());
wpsBuffer.setLength(0);
// But for the editor stream, better to show garbage than crash.
emitEditorText(editorStreamBuffer.toString());
editorStreamBuffer.setLength(0);
}
return; // Wait for more data
}

// We have a full tag: <...>
String fullTag = wpsBuffer.substring(0, gtIndex + 1);
String fullTag = editorStreamBuffer.substring(0, gtIndex + 1);
String tagName = extractTagName(fullTag);

if (HIDDEN_CONTENT_TAGS.contains(tagName)) {
Expand All @@ -169,28 +169,28 @@ private void processWpsStream(String token) {
// Actually, for a .docx, raw HTML tags might appear as text.
// Let's pass unknown tags through as text.
if (!"final".equals(tagName) && !HIDDEN_CONTENT_TAGS.contains(tagName)) {
emitWpsText(fullTag);
emitEditorText(fullTag);
}
}

// Remove the processed tag from buffer
wpsBuffer.delete(0, gtIndex + 1);
editorStreamBuffer.delete(0, gtIndex + 1);
}
} else {
// Inside Hidden Tag -> Look for the specific closing tag </tagName>
// OR self-closing />? (Protocol uses full tags mostly, except bubble_type/artifact sometimes?)
// Assuming standard </name>

String closeTag = "</" + currentHiddenTagName + ">";
int closeIndex = wpsBuffer.indexOf(closeTag);
int closeIndex = editorStreamBuffer.indexOf(closeTag);

if (closeIndex == -1) {
// Check for self-closing if strictly required?
// <bubble_type ... />
if ("bubble_type".equals(currentHiddenTagName)) {
int selfClose = wpsBuffer.indexOf("/>");
int selfClose = editorStreamBuffer.indexOf("/>");
if (selfClose != -1) {
wpsBuffer.delete(0, selfClose + 2);
editorStreamBuffer.delete(0, selfClose + 2);
isInsideHiddenTag = false;
currentHiddenTagName = null;
return;
Expand All @@ -202,14 +202,14 @@ private void processWpsStream(String token) {
// We can safely discard everything UP TO the last '<' to be safe?
// Or just keep a small window?
// To be safe: discard everything except the last few chars that might start the closing tag.
if (wpsBuffer.length() > closeTag.length() * 2) {
wpsBuffer.delete(0, wpsBuffer.length() - closeTag.length());
if (editorStreamBuffer.length() > closeTag.length() * 2) {
editorStreamBuffer.delete(0, editorStreamBuffer.length() - closeTag.length());
}
return; // Wait for more data
} else {
// Found closing tag!
// Discard everything up to and including the closing tag
wpsBuffer.delete(0, closeIndex + closeTag.length());
editorStreamBuffer.delete(0, closeIndex + closeTag.length());
isInsideHiddenTag = false;
currentHiddenTagName = null;
}
Expand All @@ -230,9 +230,9 @@ private String extractTagName(String tag) {
return content;
}

private void emitWpsText(String text) {
if (onWpsStream != null && text != null && !text.isEmpty()) {
onWpsStream.accept(text);
private void emitEditorText(String text) {
if (onEditorStream != null && text != null && !text.isEmpty()) {
onEditorStream.accept(text);
}
}

Expand Down Expand Up @@ -412,12 +412,12 @@ public void onComplete(Response<AiMessage> response) {
emitText(buffer.toString());
}

// Flush remaining WPS buffer
if (wpsBuffer.length() > 0) {
// Flush remaining editor stream buffer
if (editorStreamBuffer.length() > 0) {
// If we are left with something in buffer, it might be incomplete tag or content
// Emit it if not inside hidden tag
if (!isInsideHiddenTag) {
emitWpsText(wpsBuffer.toString());
emitEditorText(editorStreamBuffer.toString());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,21 +240,32 @@ public java.util.List<dev.langchain4j.data.message.ChatMessage> assemble(
activeContext.getId(), activeContext.getName());

String content = legalTools.read_document(activeContext.getId());

systemText.append("\n\n# Active Document (当前活跃文档)\n");
systemText.append("该文档(id=").append(activeContext.getId())
.append(", name=").append(activeContext.getName())
.append(")**已在编辑器中打开**,就是用户此刻正在看的文档。");
systemText.append("用户说\"修订一下\"\"这个文档\"\"当前文档\"或未指明对象时,默认就是指它。\n");
systemText.append("所有 doc_* 编辑/读取工具直接作用于该文档——**无需也不要**调用 ");
systemText.append("`doc_list_project_files` 或 `doc_open_file` 去重新发现/打开它;");
systemText.append("只有用户明确要操作**其他**文档时才需要那两个工具。\n\n");

if (content != null && !content.isEmpty()) {
// Truncate if too long
int maxCharsPerFile = contextProperties.getFiles().getMaxCharsPerFile();
if (content.length() > maxCharsPerFile) {
content = content.substring(0, maxCharsPerFile) + "\n... [TRUNCATED - File too long]";
}

systemText.append("\n\n# Active Document (当前活跃文档)\n");
systemText.append("The user is currently viewing/editing this document. ");
systemText.append("Use this context if the user's instruction refers to \"current document\", \"this file\", ");
systemText.append("\"line X\", \"paragraph X\", or similar positional references.\n\n");

systemText.append("<active_document id=\"").append(activeContext.getId())
.append("\" name=\"").append(activeContext.getName()).append("\"><![CDATA[\n");
systemText.append(content);
systemText.append("\n]]></active_document>\n");
} else {
// 正文暂时读不到也要保留文档标识,模型仍可用 doc_get_document_text 等工具直接读
systemText.append("<active_document id=\"").append(activeContext.getId())
.append("\" name=\"").append(activeContext.getName())
.append("\">[正文暂不可读,可用 doc_get_document_text 直接分段读取]</active_document>\n");
}
}

Expand Down
39 changes: 4 additions & 35 deletions backend/src/main/java/com/checkba/service/ai/ToolRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,43 +67,12 @@ public class ToolRegistry {
/**
* 工具名别名(旧 prompt / 老对话历史 / 模型惯性输出中出现过的名称映射到真实工具)。
*
* wps_* → doc_* 是 Phase 2.5 灰度更名(编辑器已从 WPS 迁移到 LibreOffice
* LLM 面工具名同步去 WPS 化)的兜底:老对话历史、老 prompt、模型按惯性输出的
* 旧名仍能正确分发。该批别名至少保留两个发布版本(≥0.6.0)后再评估移除
* 历史:Phase 2.5 灰度更名期间这里曾有 wps_* → doc_* 全量别名(since 0.4.x)
* 约定 ≥0.6.0 后移除,已于 0.7.9 后清理。旧名不再分发:模型输出 wps_* 会收到
* "未知工具"反馈并按系统提示改用 doc_*
*/
public static final Map<String, String> TOOL_NAME_ALIASES = Map.ofEntries(
Map.entry("search_laws", "search_web"),
// ---- Phase 2.5:WPS 时代旧工具名 → doc_*(since 0.4.x,移除不早于 0.6.0)----
Map.entry("wps_list_project_files", "doc_list_project_files"),
Map.entry("wps_open_file", "doc_open_file"),
Map.entry("wps_start_stream", "doc_start_stream"),
Map.entry("wps_get_selection", "doc_get_selection"),
Map.entry("wps_goto", "doc_goto"),
Map.entry("wps_set_selection", "doc_set_selection"),
Map.entry("wps_find_text", "doc_find_text"),
Map.entry("wps_find_replace", "doc_find_replace"),
Map.entry("wps_replace_nth_match", "doc_replace_nth_match"),
Map.entry("wps_delete_match", "doc_delete_match"),
Map.entry("wps_delete_text", "doc_delete_text"),
Map.entry("wps_replace_selection", "doc_replace_selection"),
Map.entry("wps_insert_at_cursor", "doc_insert_at_cursor"),
Map.entry("wps_get_paragraph", "doc_get_paragraph"),
Map.entry("wps_modify_paragraph", "doc_modify_paragraph"),
Map.entry("wps_get_outline", "doc_get_outline"),
Map.entry("wps_insert_under_heading", "doc_insert_under_heading"),
Map.entry("wps_search_related_docs", "doc_search_related_docs"),
Map.entry("wps_get_document_text", "doc_get_document_text"),
Map.entry("wps_get_cursor_context", "doc_get_cursor_context"),
Map.entry("wps_select_anchor", "doc_select_anchor"),
Map.entry("wps_select_paragraph", "doc_select_paragraph"),
Map.entry("wps_collapse_cursor", "doc_collapse_cursor"),
Map.entry("wps_replace_at_anchor", "doc_replace_at_anchor"),
Map.entry("wps_delete_selection", "doc_delete_selection"),
Map.entry("wps_format_selection", "doc_format_selection"),
Map.entry("wps_set_paragraph_format", "doc_set_paragraph_format"),
Map.entry("wps_undo", "doc_undo"),
Map.entry("wps_redo", "doc_redo"),
Map.entry("wps_debug_revisions", "doc_debug_revisions")
Map.entry("search_laws", "search_web")
);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
* Includes:
* 1. Search Files (Global or Scoped)
* 2. Read Files
* 3. Write Files (Text) - Registers to DB for WPS
* 4. Write Docx (MD -> DOCX) - Registers to DB for WPS
* 3. Write Files (Text) - Registers to DB for the editor
* 4. Write Docx (MD -> DOCX) - Registers to DB for the editor
*/
@Component
@Slf4j
Expand Down Expand Up @@ -188,7 +188,7 @@ public String list_files(
}

@ToolMeta(displayName = "写入文件", category = "file", fileEffect = "ADDED", fileArg = "fileName")
@Tool("Write content to a text file. Registers the file in the project database for WPS access.")
@Tool("Write content to a text file. Registers the file in the project database for editor access.")
public String write_file(
@P("Target filename (e.g. 'notes.txt')") String fileName,
@P("File content") String content,
Expand Down
Loading
Loading