feat(web): 优化子智能体线程展示
This commit is contained in:
parent
460bd97d39
commit
b991d4d75d
@ -220,127 +220,215 @@
|
||||
</div>
|
||||
|
||||
<div class="state-panel-body">
|
||||
<section class="state-section">
|
||||
<div class="state-section-header">
|
||||
<span class="state-section-title">待办</span>
|
||||
<section
|
||||
class="state-section"
|
||||
:class="{ 'is-collapsed': !isStateSectionExpanded('todos') }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="state-section-header"
|
||||
:aria-expanded="isStateSectionExpanded('todos')"
|
||||
aria-controls="state-section-todos"
|
||||
@click="toggleStateSection('todos')"
|
||||
>
|
||||
<span class="state-section-label">
|
||||
<span class="state-section-title">待办</span>
|
||||
<ChevronDown
|
||||
:size="15"
|
||||
class="state-section-chevron"
|
||||
:class="{ 'is-collapsed': !isStateSectionExpanded('todos') }"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="totalTodoCount" class="state-section-meta">
|
||||
{{ completedTodoCount }}/{{ totalTodoCount }} · {{ todoProgress }}%
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="currentTodos.length" class="todo-panel-list">
|
||||
<div
|
||||
v-for="(todo, index) in currentTodos"
|
||||
:key="`${todo.content}-${index}`"
|
||||
class="todo-item"
|
||||
>
|
||||
<div class="todo-item-icon" :class="todo.status || 'unknown'">
|
||||
<CheckCircleOutlined v-if="todo.status === 'completed'" />
|
||||
<SyncOutlined v-else-if="todo.status === 'in_progress'" spin />
|
||||
<ClockCircleOutlined v-else-if="todo.status === 'pending'" />
|
||||
<CloseCircleOutlined v-else-if="todo.status === 'cancelled'" />
|
||||
<QuestionCircleOutlined v-else />
|
||||
</div>
|
||||
<div class="todo-item-body">
|
||||
<span class="todo-item-text">{{ todo.content }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-show="isStateSectionExpanded('todos')"
|
||||
id="state-section-todos"
|
||||
class="state-section-content"
|
||||
>
|
||||
<div v-if="currentTodos.length" class="todo-panel-list">
|
||||
<div
|
||||
v-for="(todo, index) in currentTodos"
|
||||
:key="`${todo.content}-${index}`"
|
||||
class="todo-item"
|
||||
>
|
||||
<div class="todo-item-icon" :class="todo.status || 'unknown'">
|
||||
<CheckCircleOutlined v-if="todo.status === 'completed'" />
|
||||
<SyncOutlined v-else-if="todo.status === 'in_progress'" spin />
|
||||
<ClockCircleOutlined v-else-if="todo.status === 'pending'" />
|
||||
<CloseCircleOutlined v-else-if="todo.status === 'cancelled'" />
|
||||
<QuestionCircleOutlined v-else />
|
||||
</div>
|
||||
<div class="todo-item-body">
|
||||
<span class="todo-item-text">{{ todo.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="state-section-empty">暂无待办</div>
|
||||
</div>
|
||||
<div v-else class="state-section-empty">暂无待办</div>
|
||||
</section>
|
||||
|
||||
<section class="state-section">
|
||||
<div class="state-section-header">
|
||||
<span class="state-section-title">附件/文件</span>
|
||||
<section
|
||||
class="state-section"
|
||||
:class="{ 'is-collapsed': !isStateSectionExpanded('files') }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="state-section-header"
|
||||
:aria-expanded="isStateSectionExpanded('files')"
|
||||
aria-controls="state-section-files"
|
||||
@click="toggleStateSection('files')"
|
||||
>
|
||||
<span class="state-section-label">
|
||||
<span class="state-section-title">附件/文件</span>
|
||||
<ChevronDown
|
||||
:size="15"
|
||||
class="state-section-chevron"
|
||||
:class="{ 'is-collapsed': !isStateSectionExpanded('files') }"
|
||||
/>
|
||||
</span>
|
||||
<span class="state-section-meta">{{ currentStateFiles.length }}</span>
|
||||
</div>
|
||||
<div v-if="currentStateFiles.length" class="state-list">
|
||||
<div v-for="file in currentStateFiles" :key="file.key" class="state-list-item">
|
||||
<FileTypeIcon
|
||||
:name="file.name || file.path"
|
||||
:size="18"
|
||||
class="state-list-item-icon"
|
||||
/>
|
||||
<div class="state-list-item-body">
|
||||
<div class="state-list-item-title">{{ file.name }}</div>
|
||||
<div class="state-list-item-meta">{{ file.meta || file.path }}</div>
|
||||
</button>
|
||||
<div
|
||||
v-show="isStateSectionExpanded('files')"
|
||||
id="state-section-files"
|
||||
class="state-section-content"
|
||||
>
|
||||
<div v-if="currentStateFiles.length" class="state-list">
|
||||
<div v-for="file in currentStateFiles" :key="file.key" class="state-list-item">
|
||||
<FileTypeIcon
|
||||
:name="file.name || file.path"
|
||||
:size="18"
|
||||
class="state-list-item-icon"
|
||||
/>
|
||||
<div class="state-list-item-body">
|
||||
<div class="state-list-item-title">{{ file.name }}</div>
|
||||
<div class="state-list-item-meta">{{ file.meta || file.path }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="state-section-empty">暂无附件或文件</div>
|
||||
</div>
|
||||
<div v-else class="state-section-empty">暂无附件或文件</div>
|
||||
</section>
|
||||
|
||||
<section class="state-section">
|
||||
<div class="state-section-header">
|
||||
<span class="state-section-title">产物</span>
|
||||
<section
|
||||
class="state-section"
|
||||
:class="{ 'is-collapsed': !isStateSectionExpanded('artifacts') }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="state-section-header"
|
||||
:aria-expanded="isStateSectionExpanded('artifacts')"
|
||||
aria-controls="state-section-artifacts"
|
||||
@click="toggleStateSection('artifacts')"
|
||||
>
|
||||
<span class="state-section-label">
|
||||
<span class="state-section-title">产物</span>
|
||||
<ChevronDown
|
||||
:size="15"
|
||||
class="state-section-chevron"
|
||||
:class="{ 'is-collapsed': !isStateSectionExpanded('artifacts') }"
|
||||
/>
|
||||
</span>
|
||||
<span class="state-section-meta">{{ currentArtifactFiles.length }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-show="isStateSectionExpanded('artifacts')"
|
||||
id="state-section-artifacts"
|
||||
class="state-section-content"
|
||||
>
|
||||
<div v-if="currentArtifactFiles.length" class="state-list">
|
||||
<button
|
||||
v-for="file in currentArtifactFiles"
|
||||
:key="file.path"
|
||||
type="button"
|
||||
class="state-list-item state-list-item--button"
|
||||
:title="`打开 ${file.name}`"
|
||||
@click="openPanelPreview(file)"
|
||||
>
|
||||
<FileTypeIcon
|
||||
:name="file.name || file.path"
|
||||
:size="18"
|
||||
class="state-list-item-icon"
|
||||
/>
|
||||
<div class="state-list-item-body">
|
||||
<div class="state-list-item-title">{{ file.name }}</div>
|
||||
<div class="state-list-item-meta">{{ file.meta }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="state-section-empty">暂无产物</div>
|
||||
</div>
|
||||
<div v-if="currentArtifactFiles.length" class="state-list">
|
||||
<button
|
||||
v-for="file in currentArtifactFiles"
|
||||
:key="file.path"
|
||||
type="button"
|
||||
class="state-list-item state-list-item--button"
|
||||
:title="`打开 ${file.name}`"
|
||||
@click="openPanelPreview(file)"
|
||||
>
|
||||
<FileTypeIcon
|
||||
:name="file.name || file.path"
|
||||
:size="18"
|
||||
class="state-list-item-icon"
|
||||
/>
|
||||
<div class="state-list-item-body">
|
||||
<div class="state-list-item-title">{{ file.name }}</div>
|
||||
<div class="state-list-item-meta">{{ file.meta }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="state-section-empty">暂无产物</div>
|
||||
</section>
|
||||
|
||||
<section class="state-section">
|
||||
<div class="state-section-header">
|
||||
<span class="state-section-title">子智能体</span>
|
||||
<span class="state-section-meta">{{ displaySubagentRuns.length }}</span>
|
||||
</div>
|
||||
<div v-if="displaySubagentRuns.length" class="state-list">
|
||||
<div
|
||||
v-for="(run, index) in displaySubagentRuns"
|
||||
:key="run.id || `${run.subagent_type || 'subagent'}-${index}`"
|
||||
class="state-list-item"
|
||||
:class="{ 'is-clickable': run.child_thread_id }"
|
||||
@click="run.child_thread_id && openSubagentThread(run)"
|
||||
>
|
||||
<img
|
||||
v-if="getSubagentIconSrc(run)"
|
||||
class="state-subagent-icon"
|
||||
:src="getSubagentIconSrc(run)"
|
||||
:alt="`${getSubagentRunName(run)}图标`"
|
||||
<section
|
||||
class="state-section"
|
||||
:class="{ 'is-collapsed': !isStateSectionExpanded('subagents') }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="state-section-header"
|
||||
:aria-expanded="isStateSectionExpanded('subagents')"
|
||||
aria-controls="state-section-subagents"
|
||||
@click="toggleStateSection('subagents')"
|
||||
>
|
||||
<span class="state-section-label">
|
||||
<span class="state-section-title">子智能体</span>
|
||||
<ChevronDown
|
||||
:size="15"
|
||||
class="state-section-chevron"
|
||||
:class="{ 'is-collapsed': !isStateSectionExpanded('subagents') }"
|
||||
/>
|
||||
<span v-else class="state-subagent-icon" aria-hidden="true"></span>
|
||||
<div class="state-list-item-body">
|
||||
<div class="state-list-item-title state-subagent-title">
|
||||
<span>{{ getSubagentRunName(run) }}</span>
|
||||
<CheckCircleOutlined
|
||||
v-if="run.status === 'completed'"
|
||||
class="state-subagent-status-icon state-subagent-completed-icon"
|
||||
/>
|
||||
<CloseCircleOutlined
|
||||
v-else-if="run.status === 'failed'"
|
||||
class="state-subagent-status-icon state-subagent-failed-icon"
|
||||
/>
|
||||
<SyncOutlined
|
||||
v-else-if="run.status === 'running'"
|
||||
spin
|
||||
class="state-subagent-status-icon state-subagent-running-icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="state-list-item-meta">
|
||||
{{ run.description || getSubagentRunMeta(run) }}
|
||||
</span>
|
||||
<span class="state-section-meta">{{ displaySubagentRuns.length }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-show="isStateSectionExpanded('subagents')"
|
||||
id="state-section-subagents"
|
||||
class="state-section-content"
|
||||
>
|
||||
<div v-if="displaySubagentRuns.length" class="state-list">
|
||||
<div
|
||||
v-for="(run, index) in displaySubagentRuns"
|
||||
:key="run.id || `${run.subagent_type || 'subagent'}-${index}`"
|
||||
class="state-list-item"
|
||||
:class="{ 'is-clickable': run.child_thread_id }"
|
||||
@click="run.child_thread_id && openSubagentThread(run)"
|
||||
>
|
||||
<img
|
||||
v-if="getSubagentIconSrc(run)"
|
||||
class="state-subagent-icon"
|
||||
:src="getSubagentIconSrc(run)"
|
||||
:alt="`${getSubagentRunName(run)}图标`"
|
||||
/>
|
||||
<span v-else class="state-subagent-icon" aria-hidden="true"></span>
|
||||
<div class="state-list-item-body">
|
||||
<div class="state-list-item-title state-subagent-title">
|
||||
<span>{{ getSubagentRunName(run) }}</span>
|
||||
<CheckCircleOutlined
|
||||
v-if="run.status === 'completed'"
|
||||
class="state-subagent-status-icon state-subagent-completed-icon"
|
||||
/>
|
||||
<CloseCircleOutlined
|
||||
v-else-if="run.status === 'failed'"
|
||||
class="state-subagent-status-icon state-subagent-failed-icon"
|
||||
/>
|
||||
<SyncOutlined
|
||||
v-else-if="run.status === 'running'"
|
||||
spin
|
||||
class="state-subagent-status-icon state-subagent-running-icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="state-list-item-meta">
|
||||
{{ run.description || getSubagentRunMeta(run) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="state-section-empty">暂无子智能体运行</div>
|
||||
</div>
|
||||
<div v-else class="state-section-empty">暂无子智能体运行</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@ -351,7 +439,8 @@
|
||||
<SubagentThreadModal
|
||||
v-model:open="subagentThreadModal.open"
|
||||
:child-thread-id="subagentThreadModal.childThreadId"
|
||||
:subagent-name="subagentThreadModal.subagentName"
|
||||
:subagent-name="activeSubagentThreadName"
|
||||
:subagent-avatar="activeSubagentThreadAvatar"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -370,7 +459,7 @@ import {
|
||||
onDeactivated
|
||||
} from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { RefreshCw, SquareCheck } from 'lucide-vue-next'
|
||||
import { ChevronDown, RefreshCw, SquareCheck } from 'lucide-vue-next'
|
||||
import { formatFileSize } from '@/utils/file_utils'
|
||||
import FileTypeIcon from '@/components/common/FileTypeIcon.vue'
|
||||
import { generatePixelAvatar } from '@/utils/pixelAvatar'
|
||||
@ -480,6 +569,12 @@ const threadFilesMap = ref({})
|
||||
const threadAttachmentsMap = ref({})
|
||||
const attachmentUploadModalOpen = ref(false)
|
||||
const isRefreshingState = ref(false)
|
||||
const collapsedStateSections = reactive({
|
||||
todos: false,
|
||||
files: false,
|
||||
artifacts: false,
|
||||
subagents: false
|
||||
})
|
||||
const threadConfigNoticeMap = ref({})
|
||||
const threadPendingConfigNoticeMap = ref({})
|
||||
const threadConfigSnapshotMap = ref({})
|
||||
@ -787,14 +882,20 @@ const currentSubagentOptionBySlug = computed(() => {
|
||||
const subagentThreadModal = reactive({
|
||||
open: false,
|
||||
childThreadId: '',
|
||||
subagentName: ''
|
||||
subagentName: '',
|
||||
subagentAvatar: ''
|
||||
})
|
||||
const openSubagentThread = (run) => {
|
||||
if (!run?.child_thread_id) return
|
||||
subagentThreadModal.childThreadId = String(run.child_thread_id)
|
||||
subagentThreadModal.subagentName = getSubagentRunName(run)
|
||||
subagentThreadModal.subagentAvatar = getSubagentIconSrc(run)
|
||||
subagentThreadModal.open = true
|
||||
}
|
||||
const isStateSectionExpanded = (key) => !collapsedStateSections[key]
|
||||
const toggleStateSection = (key) => {
|
||||
collapsedStateSections[key] = !collapsedStateSections[key]
|
||||
}
|
||||
const currentStateFiles = computed(() => {
|
||||
const files = []
|
||||
const seenPaths = new Set()
|
||||
@ -988,6 +1089,25 @@ const displaySubagentRuns = computed(() => {
|
||||
return merged
|
||||
})
|
||||
|
||||
const activeSubagentThreadRun = computed(() => {
|
||||
if (!subagentThreadModal.childThreadId) return null
|
||||
return (
|
||||
displaySubagentRuns.value.find(
|
||||
(run) => String(run?.child_thread_id || '') === subagentThreadModal.childThreadId
|
||||
) || null
|
||||
)
|
||||
})
|
||||
const activeSubagentThreadName = computed(() =>
|
||||
activeSubagentThreadRun.value
|
||||
? getSubagentRunName(activeSubagentThreadRun.value)
|
||||
: subagentThreadModal.subagentName
|
||||
)
|
||||
const activeSubagentThreadAvatar = computed(() =>
|
||||
activeSubagentThreadRun.value
|
||||
? getSubagentIconSrc(activeSubagentThreadRun.value) || subagentThreadModal.subagentAvatar
|
||||
: subagentThreadModal.subagentAvatar
|
||||
)
|
||||
|
||||
// 首次运行的子智能体:前端按后端同样的哈希推算 child_thread_id,缓存到映射里供面板/轨迹定位。
|
||||
watch(
|
||||
onGoingConvMessages,
|
||||
@ -2858,13 +2978,45 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&.is-collapsed {
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.state-section-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 2px 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.state-section-title,
|
||||
.state-section-chevron {
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--main-200);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.state-section-label {
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.state-section-title {
|
||||
@ -2873,6 +3025,22 @@ watch(currentChatId, (threadId, oldThreadId) => {
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.state-section-chevron {
|
||||
flex-shrink: 0;
|
||||
color: var(--gray-500);
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
color 0.18s ease;
|
||||
|
||||
&.is-collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.state-section-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.state-section-empty {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="open"
|
||||
:title="subagentName || '子智能体'"
|
||||
:footer="null"
|
||||
:width="800"
|
||||
:destroyOnClose="true"
|
||||
@cancel="$emit('update:open', false)"
|
||||
>
|
||||
<template #title>
|
||||
<div class="subagent-thread-modal-title">
|
||||
<img
|
||||
v-if="subagentAvatar"
|
||||
class="subagent-thread-modal-avatar"
|
||||
:src="subagentAvatar"
|
||||
:alt="`${modalTitleName} icon`"
|
||||
/>
|
||||
<span v-else class="subagent-thread-modal-avatar" aria-hidden="true"></span>
|
||||
<span class="subagent-thread-modal-name">{{ modalTitleName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="subagent-thread-modal-body">
|
||||
<div v-if="loading" class="subagent-thread-modal-state">正在加载子智能体消息...</div>
|
||||
<div v-else-if="error" class="subagent-thread-modal-state is-error">{{ error }}</div>
|
||||
@ -16,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { agentApi } from '@/apis'
|
||||
import { MessageProcessor } from '@/utils/messageProcessor'
|
||||
import ThreadMessageList from '@/components/ThreadMessageList.vue'
|
||||
@ -33,6 +44,10 @@ const props = defineProps({
|
||||
subagentName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
subagentAvatar: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
@ -42,6 +57,8 @@ const loading = ref(false)
|
||||
const error = ref('')
|
||||
const messages = ref([])
|
||||
|
||||
const modalTitleName = computed(() => props.subagentName || '子智能体')
|
||||
|
||||
// LangChain 内容块数组 → 纯文本(仅保留 text 块)
|
||||
const flattenContent = (content) => {
|
||||
if (typeof content === 'string') return content
|
||||
@ -86,6 +103,31 @@ watch(
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.subagent-thread-modal-title {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.subagent-thread-modal-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 7px;
|
||||
background: var(--gray-0);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.subagent-thread-modal-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subagent-thread-modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
|
||||
30
web/src/utils/__tests__/subagentThread.test.js
Normal file
30
web/src/utils/__tests__/subagentThread.test.js
Normal file
@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { makeChildThreadId } from '../subagentThread.js'
|
||||
|
||||
const EXPECTED_THREAD_ID = 'subagent_198242794595efedd1850d5263f677c9b628052b1bfca0d5bc77499'
|
||||
|
||||
const run = async () => {
|
||||
assert.equal(await makeChildThreadId('thread-1', 'researcher', 'call-1'), EXPECTED_THREAD_ID)
|
||||
assert.equal(await makeChildThreadId('', 'researcher', 'call-1'), '')
|
||||
|
||||
const originalCrypto = Object.getOwnPropertyDescriptor(globalThis, 'crypto')
|
||||
try {
|
||||
Object.defineProperty(globalThis, 'crypto', {
|
||||
configurable: true,
|
||||
value: {}
|
||||
})
|
||||
|
||||
assert.equal(await makeChildThreadId('thread-1', 'researcher', 'call-1'), EXPECTED_THREAD_ID)
|
||||
} finally {
|
||||
if (originalCrypto) {
|
||||
Object.defineProperty(globalThis, 'crypto', originalCrypto)
|
||||
} else {
|
||||
delete globalThis.crypto
|
||||
}
|
||||
}
|
||||
|
||||
console.log('subagentThread: all assertions passed')
|
||||
}
|
||||
|
||||
await run()
|
||||
@ -4,12 +4,100 @@
|
||||
const PREFIX = 'subagent_'
|
||||
const DIGEST_LENGTH = 64 - PREFIX.length
|
||||
|
||||
const SHA256_K = [
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||
]
|
||||
|
||||
const rightRotate = (value, shift) => ((value >>> shift) | (value << (32 - shift))) >>> 0
|
||||
|
||||
const bytesToHex = (bytes) =>
|
||||
Array.from(bytes)
|
||||
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
|
||||
const sha256Hex = (bytes) => {
|
||||
const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64
|
||||
const padded = new Uint8Array(paddedLength)
|
||||
padded.set(bytes)
|
||||
padded[bytes.length] = 0x80
|
||||
|
||||
const view = new DataView(padded.buffer)
|
||||
const bitLength = bytes.length * 8
|
||||
view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000))
|
||||
view.setUint32(paddedLength - 4, bitLength >>> 0)
|
||||
|
||||
const words = new Uint32Array(64)
|
||||
const hash = [
|
||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
||||
]
|
||||
|
||||
for (let offset = 0; offset < paddedLength; offset += 64) {
|
||||
for (let index = 0; index < 16; index += 1) {
|
||||
words[index] = view.getUint32(offset + index * 4)
|
||||
}
|
||||
for (let index = 16; index < 64; index += 1) {
|
||||
const s0 =
|
||||
rightRotate(words[index - 15], 7) ^
|
||||
rightRotate(words[index - 15], 18) ^
|
||||
(words[index - 15] >>> 3)
|
||||
const s1 =
|
||||
rightRotate(words[index - 2], 17) ^
|
||||
rightRotate(words[index - 2], 19) ^
|
||||
(words[index - 2] >>> 10)
|
||||
words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0
|
||||
}
|
||||
|
||||
let [a, b, c, d, e, f, g, h] = hash
|
||||
for (let index = 0; index < 64; index += 1) {
|
||||
const s1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)
|
||||
const ch = (e & f) ^ (~e & g)
|
||||
const temp1 = (h + s1 + ch + SHA256_K[index] + words[index]) >>> 0
|
||||
const s0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)
|
||||
const maj = (a & b) ^ (a & c) ^ (b & c)
|
||||
const temp2 = (s0 + maj) >>> 0
|
||||
|
||||
h = g
|
||||
g = f
|
||||
f = e
|
||||
e = (d + temp1) >>> 0
|
||||
d = c
|
||||
c = b
|
||||
b = a
|
||||
a = (temp1 + temp2) >>> 0
|
||||
}
|
||||
|
||||
hash[0] = (hash[0] + a) >>> 0
|
||||
hash[1] = (hash[1] + b) >>> 0
|
||||
hash[2] = (hash[2] + c) >>> 0
|
||||
hash[3] = (hash[3] + d) >>> 0
|
||||
hash[4] = (hash[4] + e) >>> 0
|
||||
hash[5] = (hash[5] + f) >>> 0
|
||||
hash[6] = (hash[6] + g) >>> 0
|
||||
hash[7] = (hash[7] + h) >>> 0
|
||||
}
|
||||
|
||||
return hash.map((word) => word.toString(16).padStart(8, '0')).join('')
|
||||
}
|
||||
|
||||
const digestSha256Hex = async (bytes) => {
|
||||
const subtle = globalThis.crypto?.subtle
|
||||
if (typeof subtle?.digest === 'function') {
|
||||
const buffer = await subtle.digest('SHA-256', bytes)
|
||||
return bytesToHex(new Uint8Array(buffer))
|
||||
}
|
||||
return sha256Hex(bytes)
|
||||
}
|
||||
|
||||
export async function makeChildThreadId(parentThreadId, agentSlug, toolCallId) {
|
||||
if (!parentThreadId || !agentSlug || !toolCallId) return ''
|
||||
const data = new TextEncoder().encode(`${parentThreadId}:${agentSlug}:${toolCallId}`)
|
||||
const buffer = await crypto.subtle.digest('SHA-256', data)
|
||||
const hex = Array.from(new Uint8Array(buffer))
|
||||
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
const hex = await digestSha256Hex(data)
|
||||
return `${PREFIX}${hex.slice(0, DIGEST_LENGTH)}`
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user