fix(proxy): 同步当前 Notion 推理协议
Browse files- internal/proxy/notion.go +175 -48
- internal/proxy/session.go +3 -0
- internal/proxy/types.go +22 -15
internal/proxy/notion.go
CHANGED
|
@@ -1275,6 +1275,7 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
|
|
| 1275 |
session := opt.Session
|
| 1276 |
|
| 1277 |
var reqBody NotionInferenceRequest
|
|
|
|
| 1278 |
|
| 1279 |
if session != nil && session.TurnCount > 0 {
|
| 1280 |
// ── Subsequent turn: partial transcript ──
|
|
@@ -1293,31 +1294,30 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
|
|
| 1293 |
IsPartialTranscript: true,
|
| 1294 |
GenerateTitle: false,
|
| 1295 |
SaveAllThreadOperations: true,
|
| 1296 |
-
SetUnreadState: false,
|
| 1297 |
ThreadType: "workflow",
|
| 1298 |
-
AsPatchResponse: false,
|
| 1299 |
-
DebugOverrides: DebugOverrides{
|
| 1300 |
-
Model: notionModel,
|
| 1301 |
-
EmitAgentSearchExtractedResults: true,
|
| 1302 |
-
},
|
| 1303 |
}
|
| 1304 |
log.Printf("[session] subsequent turn %d on thread %s (updated-configs=%d)",
|
| 1305 |
session.TurnCount+1, session.ThreadID, len(session.UpdatedConfigIDs))
|
| 1306 |
} else {
|
| 1307 |
// ── First turn (or legacy single-turn): full transcript ──
|
| 1308 |
-
|
|
|
|
|
|
|
|
|
|
| 1309 |
if session != nil {
|
| 1310 |
// Pre-created session from HandleAnthropicMessages
|
| 1311 |
configID = session.ConfigID
|
| 1312 |
contextID = session.ContextID
|
|
|
|
| 1313 |
now = session.OriginalDatetime
|
| 1314 |
} else {
|
| 1315 |
// Legacy single-turn fallback (e.g. OpenAI-compatible handler)
|
| 1316 |
configID = generateUUIDv4()
|
| 1317 |
contextID = generateUUIDv4()
|
|
|
|
| 1318 |
now = time.Now().Format(time.RFC3339Nano)
|
| 1319 |
}
|
| 1320 |
-
transcript := buildFullTranscript(acc, messages, notionModel, disableBuiltinTools, enableWebSearch, opt.EnableWorkspaceSearch, opt.UseReadOnlyMode, attachments, configID, contextID, now)
|
| 1321 |
|
| 1322 |
// When attachments are present, reuse the upload thread instead of creating a new one.
|
| 1323 |
createThread := true
|
|
@@ -1341,13 +1341,7 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
|
|
| 1341 |
IsPartialTranscript: false,
|
| 1342 |
GenerateTitle: true,
|
| 1343 |
SaveAllThreadOperations: true,
|
| 1344 |
-
SetUnreadState: false,
|
| 1345 |
ThreadType: "workflow",
|
| 1346 |
-
AsPatchResponse: false,
|
| 1347 |
-
DebugOverrides: DebugOverrides{
|
| 1348 |
-
Model: notionModel,
|
| 1349 |
-
EmitAgentSearchExtractedResults: true,
|
| 1350 |
-
},
|
| 1351 |
}
|
| 1352 |
|
| 1353 |
if createThread {
|
|
@@ -1360,6 +1354,7 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
|
|
| 1360 |
|
| 1361 |
log.Printf("[session] first turn, thread %s (session=%v)", threadID, session != nil)
|
| 1362 |
}
|
|
|
|
| 1363 |
|
| 1364 |
bodyBytes, err := json.Marshal(reqBody)
|
| 1365 |
if err != nil {
|
|
@@ -1405,6 +1400,22 @@ func CallInference(acc *Account, messages []ChatMessage, model string, disableBu
|
|
| 1405 |
return parseNDJSONStream(reader, requestID, cb, opt.NativeToolUses, opt.ThinkingBlocks, opt.ThinkingCallback, opt.KnownCitationURLs, opt.KnownCitationDocs, opt.KnownToolCallURLs)
|
| 1406 |
}
|
| 1407 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1408 |
// buildConfigValue constructs the Notion config value map used in transcript config entries.
|
| 1409 |
// enableWorkspaceSearch: nil = use AppConfig default, non-nil = per-request override
|
| 1410 |
// useReadOnlyMode: when true, sets Notion's ASK-mode flag — model answers
|
|
@@ -1423,21 +1434,63 @@ func buildConfigValue(notionModel string, disableBuiltinTools bool, enableWebSea
|
|
| 1423 |
agentEnabled := !effectiveDisable || wsSearch
|
| 1424 |
|
| 1425 |
configValue := map[string]interface{}{
|
| 1426 |
-
"type":
|
| 1427 |
-
"
|
| 1428 |
-
"
|
| 1429 |
-
"
|
| 1430 |
-
"
|
| 1431 |
-
"
|
| 1432 |
-
"
|
| 1433 |
-
"
|
| 1434 |
-
"
|
| 1435 |
-
"
|
| 1436 |
-
"
|
| 1437 |
-
"
|
| 1438 |
-
"
|
| 1439 |
-
"
|
| 1440 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1441 |
}
|
| 1442 |
|
| 1443 |
// searchScopes controls what the built-in search tool can access
|
|
@@ -1450,14 +1503,17 @@ func buildConfigValue(notionModel string, disableBuiltinTools bool, enableWebSea
|
|
| 1450 |
}
|
| 1451 |
|
| 1452 |
if isSubsequentTurn {
|
| 1453 |
-
configValue["
|
|
|
|
|
|
|
|
|
|
| 1454 |
}
|
| 1455 |
|
| 1456 |
return configValue
|
| 1457 |
}
|
| 1458 |
|
| 1459 |
// buildContextValue constructs the Notion context value map used in transcript context entries.
|
| 1460 |
-
func buildContextValue(acc *Account, datetime string) map[string]interface{} {
|
| 1461 |
return map[string]interface{}{
|
| 1462 |
"timezone": acc.Timezone,
|
| 1463 |
"userName": acc.UserName,
|
|
@@ -1468,15 +1524,17 @@ func buildContextValue(acc *Account, datetime string) map[string]interface{} {
|
|
| 1468 |
"spaceViewId": acc.SpaceViewID,
|
| 1469 |
"currentDatetime": datetime,
|
| 1470 |
"surface": "ai_module",
|
|
|
|
|
|
|
| 1471 |
}
|
| 1472 |
}
|
| 1473 |
|
| 1474 |
// buildFullTranscript builds a complete transcript for the first turn of a conversation.
|
| 1475 |
// Uses ResearcherTranscriptMsg (with id field) to match Notion's real client format.
|
| 1476 |
-
func buildFullTranscript(acc *Account, messages []ChatMessage, notionModel string, disableBuiltinTools bool, enableWebSearch bool, enableWorkspaceSearch *bool, useReadOnlyMode bool, attachments []UploadedAttachment, configID, contextID, now string) []interface{} {
|
| 1477 |
hasAttachments := len(attachments) > 0
|
| 1478 |
configValue := buildConfigValue(notionModel, disableBuiltinTools, enableWebSearch, enableWorkspaceSearch, useReadOnlyMode, hasAttachments, false)
|
| 1479 |
-
contextValue := buildContextValue(acc, now)
|
| 1480 |
|
| 1481 |
if hasAttachments {
|
| 1482 |
contextValue["surface"] = "workflows"
|
|
@@ -1555,7 +1613,11 @@ func buildFullTranscript(acc *Account, messages []ChatMessage, notionModel strin
|
|
| 1555 |
// It includes: config + context (reused IDs) + N updated-config placeholders + new user message.
|
| 1556 |
func buildPartialTranscript(acc *Account, newUserContent string, notionModel string, disableBuiltinTools bool, enableWebSearch bool, enableWorkspaceSearch *bool, useReadOnlyMode bool, session *Session) []interface{} {
|
| 1557 |
configValue := buildConfigValue(notionModel, disableBuiltinTools, enableWebSearch, enableWorkspaceSearch, useReadOnlyMode, false, true)
|
| 1558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1559 |
|
| 1560 |
transcript := []interface{}{
|
| 1561 |
ResearcherTranscriptMsg{
|
|
@@ -1568,6 +1630,11 @@ func buildPartialTranscript(acc *Account, newUserContent string, notionModel str
|
|
| 1568 |
Type: "context",
|
| 1569 |
Value: contextValue,
|
| 1570 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1571 |
}
|
| 1572 |
|
| 1573 |
// Add updated-config placeholders for each previous turn
|
|
@@ -1584,12 +1651,19 @@ func buildPartialTranscript(acc *Account, newUserContent string, notionModel str
|
|
| 1584 |
Type: "user",
|
| 1585 |
Value: [][]string{{newUserContent}},
|
| 1586 |
UserID: acc.UserID,
|
| 1587 |
-
CreatedAt:
|
| 1588 |
})
|
| 1589 |
|
| 1590 |
return transcript
|
| 1591 |
}
|
| 1592 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1593 |
func setNotionHeaders(req *http.Request, acc *Account) {
|
| 1594 |
// Content negotiation
|
| 1595 |
req.Header.Set("Content-Type", "application/json")
|
|
@@ -2309,6 +2383,7 @@ func parseNDJSONStream(reader io.Reader, requestID string, cb StreamCallback, na
|
|
| 2309 |
patchValueTypes := make(map[string]string)
|
| 2310 |
// Counter: "/s/N" → how many value entries added so far
|
| 2311 |
patchValueCounts := make(map[string]int)
|
|
|
|
| 2312 |
// Accumulated thinking content from patch operations
|
| 2313 |
var patchThinkingContent string
|
| 2314 |
var patchThinkingSignature string
|
|
@@ -2392,6 +2467,37 @@ func parseNDJSONStream(reader io.Reader, requestID string, cb StreamCallback, na
|
|
| 2392 |
sentClean = cleaned
|
| 2393 |
}
|
| 2394 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2395 |
for scanner.Scan() {
|
| 2396 |
line := strings.TrimSpace(scanner.Text())
|
| 2397 |
if line == "" {
|
|
@@ -2635,23 +2741,42 @@ func parseNDJSONStream(reader io.Reader, requestID string, cb StreamCallback, na
|
|
| 2635 |
cb("", true, &totalUsage)
|
| 2636 |
}
|
| 2637 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2638 |
case "patch":
|
| 2639 |
var patch PatchEvent
|
| 2640 |
if err := json.Unmarshal([]byte(line), &patch); err != nil {
|
| 2641 |
continue
|
| 2642 |
}
|
| 2643 |
for _, op := range patch.V {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2644 |
// Track value entry types when new entries are added
|
| 2645 |
if op.O == "a" && strings.Contains(op.P, "/value/-") {
|
| 2646 |
-
var entry
|
| 2647 |
-
|
| 2648 |
-
}
|
| 2649 |
-
if json.Unmarshal(op.V, &entry) == nil && entry.Type != "" {
|
| 2650 |
statePrefix := op.P[:strings.Index(op.P, "/value/")]
|
| 2651 |
idx := patchValueCounts[statePrefix]
|
| 2652 |
-
|
| 2653 |
-
patchValueTypes[path] = entry.Type
|
| 2654 |
-
patchValueCounts[statePrefix] = idx + 1
|
| 2655 |
}
|
| 2656 |
}
|
| 2657 |
|
|
@@ -2666,12 +2791,14 @@ func parseNDJSONStream(reader io.Reader, requestID string, cb StreamCallback, na
|
|
| 2666 |
}
|
| 2667 |
|
| 2668 |
// Handle finishedAt for thinking → flush thinking block
|
| 2669 |
-
if op.O == "a" && strings.
|
| 2670 |
-
if
|
| 2671 |
-
|
| 2672 |
-
|
| 2673 |
-
|
| 2674 |
-
|
|
|
|
|
|
|
| 2675 |
patchThinkingContent = ""
|
| 2676 |
patchThinkingSignature = ""
|
| 2677 |
}
|
|
|
|
| 1275 |
session := opt.Session
|
| 1276 |
|
| 1277 |
var reqBody NotionInferenceRequest
|
| 1278 |
+
createdSource := "ai_module"
|
| 1279 |
|
| 1280 |
if session != nil && session.TurnCount > 0 {
|
| 1281 |
// ── Subsequent turn: partial transcript ──
|
|
|
|
| 1294 |
IsPartialTranscript: true,
|
| 1295 |
GenerateTitle: false,
|
| 1296 |
SaveAllThreadOperations: true,
|
|
|
|
| 1297 |
ThreadType: "workflow",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1298 |
}
|
| 1299 |
log.Printf("[session] subsequent turn %d on thread %s (updated-configs=%d)",
|
| 1300 |
session.TurnCount+1, session.ThreadID, len(session.UpdatedConfigIDs))
|
| 1301 |
} else {
|
| 1302 |
// ── First turn (or legacy single-turn): full transcript ──
|
| 1303 |
+
if len(attachments) > 0 {
|
| 1304 |
+
createdSource = "workflows"
|
| 1305 |
+
}
|
| 1306 |
+
var configID, contextID, contextPageID, now string
|
| 1307 |
if session != nil {
|
| 1308 |
// Pre-created session from HandleAnthropicMessages
|
| 1309 |
configID = session.ConfigID
|
| 1310 |
contextID = session.ContextID
|
| 1311 |
+
contextPageID = ensureSessionContextPageID(session)
|
| 1312 |
now = session.OriginalDatetime
|
| 1313 |
} else {
|
| 1314 |
// Legacy single-turn fallback (e.g. OpenAI-compatible handler)
|
| 1315 |
configID = generateUUIDv4()
|
| 1316 |
contextID = generateUUIDv4()
|
| 1317 |
+
contextPageID = generateUUIDv4()
|
| 1318 |
now = time.Now().Format(time.RFC3339Nano)
|
| 1319 |
}
|
| 1320 |
+
transcript := buildFullTranscript(acc, messages, notionModel, disableBuiltinTools, enableWebSearch, opt.EnableWorkspaceSearch, opt.UseReadOnlyMode, attachments, configID, contextID, contextPageID, now)
|
| 1321 |
|
| 1322 |
// When attachments are present, reuse the upload thread instead of creating a new one.
|
| 1323 |
createThread := true
|
|
|
|
| 1341 |
IsPartialTranscript: false,
|
| 1342 |
GenerateTitle: true,
|
| 1343 |
SaveAllThreadOperations: true,
|
|
|
|
| 1344 |
ThreadType: "workflow",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1345 |
}
|
| 1346 |
|
| 1347 |
if createThread {
|
|
|
|
| 1354 |
|
| 1355 |
log.Printf("[session] first turn, thread %s (session=%v)", threadID, session != nil)
|
| 1356 |
}
|
| 1357 |
+
applyWorkflowRequestProtocol(&reqBody, createdSource)
|
| 1358 |
|
| 1359 |
bodyBytes, err := json.Marshal(reqBody)
|
| 1360 |
if err != nil {
|
|
|
|
| 1400 |
return parseNDJSONStream(reader, requestID, cb, opt.NativeToolUses, opt.ThinkingBlocks, opt.ThinkingCallback, opt.KnownCitationURLs, opt.KnownCitationDocs, opt.KnownToolCallURLs)
|
| 1401 |
}
|
| 1402 |
|
| 1403 |
+
func applyWorkflowRequestProtocol(reqBody *NotionInferenceRequest, createdSource string) {
|
| 1404 |
+
reqBody.SetUnreadState = true
|
| 1405 |
+
reqBody.CreatedSource = createdSource
|
| 1406 |
+
reqBody.AsPatchResponse = true
|
| 1407 |
+
reqBody.PatchResponseVersion = 2
|
| 1408 |
+
reqBody.IsUserInAnySalesAssistedSpace = boolPtr(false)
|
| 1409 |
+
reqBody.IsSpaceSalesAssisted = boolPtr(false)
|
| 1410 |
+
reqBody.SupportsCustomAgentNudgeTranscriptStep = boolPtr(true)
|
| 1411 |
+
reqBody.DebugOverrides = DebugOverrides{
|
| 1412 |
+
EmitAgentSearchExtractedResults: true,
|
| 1413 |
+
CachedInferences: &struct{}{},
|
| 1414 |
+
AnnotationInferences: &struct{}{},
|
| 1415 |
+
EmitInferences: boolPtr(false),
|
| 1416 |
+
}
|
| 1417 |
+
}
|
| 1418 |
+
|
| 1419 |
// buildConfigValue constructs the Notion config value map used in transcript config entries.
|
| 1420 |
// enableWorkspaceSearch: nil = use AppConfig default, non-nil = per-request override
|
| 1421 |
// useReadOnlyMode: when true, sets Notion's ASK-mode flag — model answers
|
|
|
|
| 1434 |
agentEnabled := !effectiveDisable || wsSearch
|
| 1435 |
|
| 1436 |
configValue := map[string]interface{}{
|
| 1437 |
+
"type": "workflow",
|
| 1438 |
+
"enableAgentAutomations": agentEnabled,
|
| 1439 |
+
"enableAgentIntegrations": agentEnabled,
|
| 1440 |
+
"enableCustomAgents": !effectiveDisable,
|
| 1441 |
+
"enableExperimentalIntegrations": false,
|
| 1442 |
+
"enableScriptAgent": !effectiveDisable,
|
| 1443 |
+
"enableScriptAgentAdvanced": false,
|
| 1444 |
+
"enableScriptAgentSearchConnectorsInCustomAgent": false,
|
| 1445 |
+
"enableScriptAgentGoogleDriveInCustomAgent": false,
|
| 1446 |
+
"enableScriptAgentGoogleDriveOAuthInCustomAgent": false,
|
| 1447 |
+
"enableScriptAgentSlack": !effectiveDisable,
|
| 1448 |
+
"enableScriptAgentMcpServers": !effectiveDisable,
|
| 1449 |
+
"enableAgentDiffs": !effectiveDisable,
|
| 1450 |
+
"enableCsvAttachmentSupport": true,
|
| 1451 |
+
"showDatabaseAgentsDiscoverability": true,
|
| 1452 |
+
"enableAgentThreadTools": false,
|
| 1453 |
+
"enableCrdtOperations": false,
|
| 1454 |
+
"enableAgentCardCustomization": true,
|
| 1455 |
+
"enableSystemPromptAsPage": false,
|
| 1456 |
+
"enableUserSessionContext": false,
|
| 1457 |
+
"enableLargeToolResultComputerOffload": false,
|
| 1458 |
+
"enableScriptAgentGtm": false,
|
| 1459 |
+
"enablePitCrewTableViewTool": false,
|
| 1460 |
+
"enableComputer": !effectiveDisable,
|
| 1461 |
+
"enableCustomAgentCreateGuidanceV2": true,
|
| 1462 |
+
"enableSoftwareFactoryPage": false,
|
| 1463 |
+
"enableAgentGenerateImage": !effectiveDisable,
|
| 1464 |
+
"enableQueryCalendar": false,
|
| 1465 |
+
"enableQueryMail": false,
|
| 1466 |
+
"enableMailExplicitToolCalls": true,
|
| 1467 |
+
"enableMailNotificationPreferences": false,
|
| 1468 |
+
"enableMailAgentMultiProviderSupport": true,
|
| 1469 |
+
"enableNotionMailDeprecated": false,
|
| 1470 |
+
"enableWebResearch": false,
|
| 1471 |
+
"useRulePrioritization": true,
|
| 1472 |
+
"useWebSearch": enableWebSearch,
|
| 1473 |
+
"isHipaa": false,
|
| 1474 |
+
"internetAccess": false,
|
| 1475 |
+
"manageWorkers": false,
|
| 1476 |
+
"useReadOnlyMode": useReadOnlyMode,
|
| 1477 |
+
"writerMode": false,
|
| 1478 |
+
"model": notionModel,
|
| 1479 |
+
"modelFromUser": true,
|
| 1480 |
+
"isCustomAgent": false,
|
| 1481 |
+
"isCustomAgentBuilder": false,
|
| 1482 |
+
"isCustomAgentCreate": false,
|
| 1483 |
+
"isAgentResearchRequest": false,
|
| 1484 |
+
"useCustomAgentDraft": false,
|
| 1485 |
+
"enableMarkdownVNext": false,
|
| 1486 |
+
"enableAgentSkillsV2": false,
|
| 1487 |
+
"updatePageStaleViewGuardEnabled": false,
|
| 1488 |
+
"enableUpdatePageOrderUpdates": true,
|
| 1489 |
+
"enableAgentSupportPropertyReorder": true,
|
| 1490 |
+
"enableAgentAskSurvey": true,
|
| 1491 |
+
"databaseAgentConfigMode": false,
|
| 1492 |
+
"isOnboardingAgent": false,
|
| 1493 |
+
"isMobile": false,
|
| 1494 |
}
|
| 1495 |
|
| 1496 |
// searchScopes controls what the built-in search tool can access
|
|
|
|
| 1503 |
}
|
| 1504 |
|
| 1505 |
if isSubsequentTurn {
|
| 1506 |
+
configValue["useContextualCoreDocsAutoLoad"] = false
|
| 1507 |
+
configValue["useDocPreviewsForCoreAutoLoad"] = true
|
| 1508 |
+
} else {
|
| 1509 |
+
configValue["availableConnectors"] = []interface{}{}
|
| 1510 |
}
|
| 1511 |
|
| 1512 |
return configValue
|
| 1513 |
}
|
| 1514 |
|
| 1515 |
// buildContextValue constructs the Notion context value map used in transcript context entries.
|
| 1516 |
+
func buildContextValue(acc *Account, datetime, contextPageID string) map[string]interface{} {
|
| 1517 |
return map[string]interface{}{
|
| 1518 |
"timezone": acc.Timezone,
|
| 1519 |
"userName": acc.UserName,
|
|
|
|
| 1524 |
"spaceViewId": acc.SpaceViewID,
|
| 1525 |
"currentDatetime": datetime,
|
| 1526 |
"surface": "ai_module",
|
| 1527 |
+
"agentAccessory": "paprika",
|
| 1528 |
+
"context_page_id": contextPageID,
|
| 1529 |
}
|
| 1530 |
}
|
| 1531 |
|
| 1532 |
// buildFullTranscript builds a complete transcript for the first turn of a conversation.
|
| 1533 |
// Uses ResearcherTranscriptMsg (with id field) to match Notion's real client format.
|
| 1534 |
+
func buildFullTranscript(acc *Account, messages []ChatMessage, notionModel string, disableBuiltinTools bool, enableWebSearch bool, enableWorkspaceSearch *bool, useReadOnlyMode bool, attachments []UploadedAttachment, configID, contextID, contextPageID, now string) []interface{} {
|
| 1535 |
hasAttachments := len(attachments) > 0
|
| 1536 |
configValue := buildConfigValue(notionModel, disableBuiltinTools, enableWebSearch, enableWorkspaceSearch, useReadOnlyMode, hasAttachments, false)
|
| 1537 |
+
contextValue := buildContextValue(acc, now, contextPageID)
|
| 1538 |
|
| 1539 |
if hasAttachments {
|
| 1540 |
contextValue["surface"] = "workflows"
|
|
|
|
| 1613 |
// It includes: config + context (reused IDs) + N updated-config placeholders + new user message.
|
| 1614 |
func buildPartialTranscript(acc *Account, newUserContent string, notionModel string, disableBuiltinTools bool, enableWebSearch bool, enableWorkspaceSearch *bool, useReadOnlyMode bool, session *Session) []interface{} {
|
| 1615 |
configValue := buildConfigValue(notionModel, disableBuiltinTools, enableWebSearch, enableWorkspaceSearch, useReadOnlyMode, false, true)
|
| 1616 |
+
contextPageID := ensureSessionContextPageID(session)
|
| 1617 |
+
contextValue := buildContextValue(acc, session.OriginalDatetime, contextPageID)
|
| 1618 |
+
currentDatetime := time.Now().Format(time.RFC3339Nano)
|
| 1619 |
+
currentContextValue := buildContextValue(acc, currentDatetime, contextPageID)
|
| 1620 |
+
currentContextValue["surface"] = "full_page_chat"
|
| 1621 |
|
| 1622 |
transcript := []interface{}{
|
| 1623 |
ResearcherTranscriptMsg{
|
|
|
|
| 1630 |
Type: "context",
|
| 1631 |
Value: contextValue,
|
| 1632 |
},
|
| 1633 |
+
ResearcherTranscriptMsg{
|
| 1634 |
+
ID: generateUUIDv4(),
|
| 1635 |
+
Type: "context",
|
| 1636 |
+
Value: currentContextValue,
|
| 1637 |
+
},
|
| 1638 |
}
|
| 1639 |
|
| 1640 |
// Add updated-config placeholders for each previous turn
|
|
|
|
| 1651 |
Type: "user",
|
| 1652 |
Value: [][]string{{newUserContent}},
|
| 1653 |
UserID: acc.UserID,
|
| 1654 |
+
CreatedAt: currentDatetime,
|
| 1655 |
})
|
| 1656 |
|
| 1657 |
return transcript
|
| 1658 |
}
|
| 1659 |
|
| 1660 |
+
func ensureSessionContextPageID(session *Session) string {
|
| 1661 |
+
if session.ContextPageID == "" {
|
| 1662 |
+
session.ContextPageID = generateUUIDv4()
|
| 1663 |
+
}
|
| 1664 |
+
return session.ContextPageID
|
| 1665 |
+
}
|
| 1666 |
+
|
| 1667 |
func setNotionHeaders(req *http.Request, acc *Account) {
|
| 1668 |
// Content negotiation
|
| 1669 |
req.Header.Set("Content-Type", "application/json")
|
|
|
|
| 2383 |
patchValueTypes := make(map[string]string)
|
| 2384 |
// Counter: "/s/N" → how many value entries added so far
|
| 2385 |
patchValueCounts := make(map[string]int)
|
| 2386 |
+
patchNextStepIndex := 0
|
| 2387 |
// Accumulated thinking content from patch operations
|
| 2388 |
var patchThinkingContent string
|
| 2389 |
var patchThinkingSignature string
|
|
|
|
| 2467 |
sentClean = cleaned
|
| 2468 |
}
|
| 2469 |
|
| 2470 |
+
handlePatchValueEntry := func(statePrefix string, index int, entry AgentValueEntry) {
|
| 2471 |
+
patchValueTypes[fmt.Sprintf("%s/value/%d", statePrefix, index)] = entry.Type
|
| 2472 |
+
if patchValueCounts[statePrefix] <= index {
|
| 2473 |
+
patchValueCounts[statePrefix] = index + 1
|
| 2474 |
+
}
|
| 2475 |
+
|
| 2476 |
+
switch entry.Type {
|
| 2477 |
+
case "thinking":
|
| 2478 |
+
prev := patchThinkingContent
|
| 2479 |
+
patchThinkingContent += entry.Content
|
| 2480 |
+
if entry.Signature != "" {
|
| 2481 |
+
patchThinkingSignature = entry.Signature
|
| 2482 |
+
lastThinkingSignature = entry.Signature
|
| 2483 |
+
}
|
| 2484 |
+
emitThinking(incrementalSuffix(prev, patchThinkingContent))
|
| 2485 |
+
case "text":
|
| 2486 |
+
if entry.Content != "" {
|
| 2487 |
+
rawText += entry.Content
|
| 2488 |
+
emitDelta()
|
| 2489 |
+
}
|
| 2490 |
+
case "tool_use":
|
| 2491 |
+
// Skip — tool state must not pollute text output.
|
| 2492 |
+
if entry.Name != "" && entry.ID != "" && !seenNativeToolUseIDs[entry.ID] {
|
| 2493 |
+
seenNativeToolUseIDs[entry.ID] = true
|
| 2494 |
+
if nativeToolUses != nil {
|
| 2495 |
+
*nativeToolUses = append(*nativeToolUses, entry)
|
| 2496 |
+
}
|
| 2497 |
+
}
|
| 2498 |
+
}
|
| 2499 |
+
}
|
| 2500 |
+
|
| 2501 |
for scanner.Scan() {
|
| 2502 |
line := strings.TrimSpace(scanner.Text())
|
| 2503 |
if line == "" {
|
|
|
|
| 2741 |
cb("", true, &totalUsage)
|
| 2742 |
}
|
| 2743 |
|
| 2744 |
+
case "patch-start":
|
| 2745 |
+
var patchStart struct {
|
| 2746 |
+
Data struct {
|
| 2747 |
+
Steps []json.RawMessage `json:"s"`
|
| 2748 |
+
} `json:"data"`
|
| 2749 |
+
}
|
| 2750 |
+
if json.Unmarshal([]byte(line), &patchStart) == nil {
|
| 2751 |
+
patchNextStepIndex = len(patchStart.Data.Steps)
|
| 2752 |
+
}
|
| 2753 |
+
|
| 2754 |
case "patch":
|
| 2755 |
var patch PatchEvent
|
| 2756 |
if err := json.Unmarshal([]byte(line), &patch); err != nil {
|
| 2757 |
continue
|
| 2758 |
}
|
| 2759 |
for _, op := range patch.V {
|
| 2760 |
+
if op.O == "a" && op.P == "/s/-" {
|
| 2761 |
+
stepIndex := patchNextStepIndex
|
| 2762 |
+
patchNextStepIndex++
|
| 2763 |
+
|
| 2764 |
+
var step AgentInferenceEvent
|
| 2765 |
+
if json.Unmarshal(op.V, &step) == nil && step.Type == "agent-inference" {
|
| 2766 |
+
statePrefix := fmt.Sprintf("/s/%d", stepIndex)
|
| 2767 |
+
for idx, entry := range step.Value {
|
| 2768 |
+
handlePatchValueEntry(statePrefix, idx, entry)
|
| 2769 |
+
}
|
| 2770 |
+
}
|
| 2771 |
+
}
|
| 2772 |
+
|
| 2773 |
// Track value entry types when new entries are added
|
| 2774 |
if op.O == "a" && strings.Contains(op.P, "/value/-") {
|
| 2775 |
+
var entry AgentValueEntry
|
| 2776 |
+
if json.Unmarshal(op.V, &entry) == nil {
|
|
|
|
|
|
|
| 2777 |
statePrefix := op.P[:strings.Index(op.P, "/value/")]
|
| 2778 |
idx := patchValueCounts[statePrefix]
|
| 2779 |
+
handlePatchValueEntry(statePrefix, idx, entry)
|
|
|
|
|
|
|
| 2780 |
}
|
| 2781 |
}
|
| 2782 |
|
|
|
|
| 2791 |
}
|
| 2792 |
|
| 2793 |
// Handle finishedAt for thinking → flush thinking block
|
| 2794 |
+
if op.O == "a" && strings.HasSuffix(op.P, "/finishedAt") {
|
| 2795 |
+
if patchThinkingContent != "" {
|
| 2796 |
+
if thinkingBlocks != nil {
|
| 2797 |
+
*thinkingBlocks = append(*thinkingBlocks, ThinkingBlock{
|
| 2798 |
+
Content: patchThinkingContent,
|
| 2799 |
+
Signature: patchThinkingSignature,
|
| 2800 |
+
})
|
| 2801 |
+
}
|
| 2802 |
patchThinkingContent = ""
|
| 2803 |
patchThinkingSignature = ""
|
| 2804 |
}
|
internal/proxy/session.go
CHANGED
|
@@ -23,6 +23,9 @@ type Session struct {
|
|
| 23 |
ConfigID string
|
| 24 |
ContextID string
|
| 25 |
|
|
|
|
|
|
|
|
|
|
| 26 |
// Each completed turn produces one updated-config placeholder ID
|
| 27 |
UpdatedConfigIDs []string
|
| 28 |
|
|
|
|
| 23 |
ConfigID string
|
| 24 |
ContextID string
|
| 25 |
|
| 26 |
+
// ContextPageID is generated independently from the transcript IDs and reused across turns.
|
| 27 |
+
ContextPageID string
|
| 28 |
+
|
| 29 |
// Each completed turn produces one updated-config placeholder ID
|
| 30 |
UpdatedConfigIDs []string
|
| 31 |
|
internal/proxy/types.go
CHANGED
|
@@ -150,19 +150,24 @@ type UsageInfo struct {
|
|
| 150 |
// ========== Notion API Types ==========
|
| 151 |
|
| 152 |
type NotionInferenceRequest struct {
|
| 153 |
-
TraceID
|
| 154 |
-
SpaceID
|
| 155 |
-
ThreadID
|
| 156 |
-
Transcript
|
| 157 |
-
CreateThread
|
| 158 |
-
GenerateTitle
|
| 159 |
-
SaveAllThreadOperations
|
| 160 |
-
SetUnreadState
|
| 161 |
-
ThreadType
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
// ThreadParentPointer identifies the parent of a thread (used only on first turn)
|
|
@@ -194,8 +199,10 @@ type ResearcherTranscriptMsg struct {
|
|
| 194 |
}
|
| 195 |
|
| 196 |
type DebugOverrides struct {
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
| 199 |
}
|
| 200 |
|
| 201 |
type NDJSONEvent struct {
|
|
|
|
| 150 |
// ========== Notion API Types ==========
|
| 151 |
|
| 152 |
type NotionInferenceRequest struct {
|
| 153 |
+
TraceID string `json:"traceId"`
|
| 154 |
+
SpaceID string `json:"spaceId"`
|
| 155 |
+
ThreadID string `json:"threadId,omitempty"`
|
| 156 |
+
Transcript []interface{} `json:"transcript"`
|
| 157 |
+
CreateThread bool `json:"createThread"`
|
| 158 |
+
GenerateTitle bool `json:"generateTitle"`
|
| 159 |
+
SaveAllThreadOperations bool `json:"saveAllThreadOperations"`
|
| 160 |
+
SetUnreadState bool `json:"setUnreadState"`
|
| 161 |
+
ThreadType string `json:"threadType"`
|
| 162 |
+
CreatedSource string `json:"createdSource,omitempty"`
|
| 163 |
+
AsPatchResponse bool `json:"asPatchResponse"`
|
| 164 |
+
PatchResponseVersion int `json:"patchResponseVersion,omitempty"`
|
| 165 |
+
IsPartialTranscript bool `json:"isPartialTranscript"`
|
| 166 |
+
IsUserInAnySalesAssistedSpace *bool `json:"isUserInAnySalesAssistedSpace,omitempty"`
|
| 167 |
+
IsSpaceSalesAssisted *bool `json:"isSpaceSalesAssisted,omitempty"`
|
| 168 |
+
SupportsCustomAgentNudgeTranscriptStep *bool `json:"supportsCustomAgentNudgeTranscriptStep,omitempty"`
|
| 169 |
+
ThreadParentPointer *ThreadParentPointer `json:"threadParentPointer,omitempty"`
|
| 170 |
+
DebugOverrides DebugOverrides `json:"debugOverrides"`
|
| 171 |
}
|
| 172 |
|
| 173 |
// ThreadParentPointer identifies the parent of a thread (used only on first turn)
|
|
|
|
| 199 |
}
|
| 200 |
|
| 201 |
type DebugOverrides struct {
|
| 202 |
+
EmitAgentSearchExtractedResults bool `json:"emitAgentSearchExtractedResults,omitempty"`
|
| 203 |
+
CachedInferences *struct{} `json:"cachedInferences,omitempty"`
|
| 204 |
+
AnnotationInferences *struct{} `json:"annotationInferences,omitempty"`
|
| 205 |
+
EmitInferences *bool `json:"emitInferences,omitempty"`
|
| 206 |
}
|
| 207 |
|
| 208 |
type NDJSONEvent struct {
|