mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
feat: Support LLM Tracing by Alibaba Cloud EDAS product (#7895)
* feat: add tracing modules * Initial commit * fix: problem * fix: update trace web * fix: trace view * fix: trace view * fix: fix some problem * fix: knowledge and mcp trace * feat: save trace to user home dir * feat: open trace with electron browser window * fix: root trace outputs * feat: trace internationalization and add trace icon * feat: add trace title * feat: update * package.json添加windows运行script * feat: update window title * fix: mcp trace param * fix: error show * fix: listTool result * fix: merge error * feat: add stream usage and response * feat: change trace stream * fix: change stream adapter * fix: span detail show problem * fix: process show by time * fix: stream outputs * fix: merge problem * fix: stream outputs * fix: output text * fix: EDAS support text * fix: change trace footer style * fix: topicId is loaded multiple times * fix: span reload problem & attribute with cache * fix: refresh optimization * Change Powered by text. * resolve upstream conflicts * fix: build-time type exception * fix: exceptions not used when building * fix: recend no trace * fix: resend trace list * fix: delete temporary files * feat: trace for resend * fix: trace for resend message with edit * fix: directory structure and construction method of mcp-trace * fix: change CRLF to LF * fix: add function call outputs * Revert "fix: change CRLF to LF" * fix: reorganize multi-model display * fix: append model trace binding topic * fix: some problems * fix: code optimization * fix: delete async * fix: UI optimization * fix: sort import --------- Co-authored-by: 崔顺发 <csf01409784@alibaba-inc.com> Co-authored-by: 管鑫荣 <gxr01409783@alibaba-inc.com>
This commit is contained in:
parent
411c5bc94e
commit
3b123863b5
1
.gitignore
vendored
1
.gitignore
vendored
@ -35,6 +35,7 @@ Thumbs.db
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
mcp_server
|
||||
stats.html
|
||||
|
||||
# ENV
|
||||
|
||||
@ -19,7 +19,9 @@ export default defineConfig({
|
||||
'@main': resolve('src/main'),
|
||||
'@types': resolve('src/renderer/src/types'),
|
||||
'@shared': resolve('packages/shared'),
|
||||
'@logger': resolve('src/main/services/LoggerService')
|
||||
'@logger': resolve('src/main/services/LoggerService'),
|
||||
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
||||
'@mcp-trace/trace-node': resolve('packages/mcp-trace/trace-node')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
@ -40,10 +42,16 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
plugins: [
|
||||
react({
|
||||
tsDecorators: true
|
||||
}),
|
||||
externalizeDepsPlugin()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@shared': resolve('packages/shared')
|
||||
'@shared': resolve('packages/shared'),
|
||||
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
@ -53,6 +61,7 @@ export default defineConfig({
|
||||
renderer: {
|
||||
plugins: [
|
||||
react({
|
||||
tsDecorators: true,
|
||||
plugins: [
|
||||
[
|
||||
'@swc/plugin-styled-components',
|
||||
@ -72,7 +81,9 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@renderer': resolve('src/renderer/src'),
|
||||
'@shared': resolve('packages/shared'),
|
||||
'@logger': resolve('src/renderer/src/services/LoggerService')
|
||||
'@logger': resolve('src/renderer/src/services/LoggerService'),
|
||||
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
||||
'@mcp-trace/trace-web': resolve('packages/mcp-trace/trace-web')
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
@ -91,7 +102,8 @@ export default defineConfig({
|
||||
index: resolve(__dirname, 'src/renderer/index.html'),
|
||||
miniWindow: resolve(__dirname, 'src/renderer/miniWindow.html'),
|
||||
selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'),
|
||||
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html')
|
||||
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html'),
|
||||
traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
16
package.json
16
package.json
@ -13,7 +13,10 @@
|
||||
],
|
||||
"installConfig": {
|
||||
"hoistingLimits": [
|
||||
"packages/database"
|
||||
"packages/database",
|
||||
"packages/mcp-trace/trace-core",
|
||||
"packages/mcp-trace/trace-node",
|
||||
"packages/mcp-trace/trace-web"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -38,6 +41,7 @@
|
||||
"publish": "yarn build:check && yarn release patch push",
|
||||
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
||||
"generate:agents": "yarn workspace @cherry-studio/database agents",
|
||||
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
||||
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
||||
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
@ -74,6 +78,7 @@
|
||||
"notion-helper": "^1.3.22",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"pdfjs-dist": "4.10.38",
|
||||
"react-json-view": "^1.21.3",
|
||||
"selection-hook": "^1.0.7",
|
||||
"turndown": "7.2.0"
|
||||
},
|
||||
@ -114,6 +119,12 @@
|
||||
"@modelcontextprotocol/sdk": "^1.12.3",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "2.0.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@shikijs/markdown-it": "^3.7.0",
|
||||
@ -195,7 +206,7 @@
|
||||
"mime": "^4.0.4",
|
||||
"motion": "^12.10.5",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"officeparser": "^4.2.0",
|
||||
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
|
||||
"p-queue": "^8.1.0",
|
||||
"playwright": "^1.52.0",
|
||||
@ -216,6 +227,7 @@
|
||||
"react-window": "^1.8.11",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-mathjax": "^7.1.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
26
packages/mcp-trace/trace-core/core/spanConvert.ts
Normal file
26
packages/mcp-trace/trace-core/core/spanConvert.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { SpanKind, SpanStatusCode } from '@opentelemetry/api'
|
||||
import { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||
|
||||
import { SpanEntity } from '../types/config'
|
||||
|
||||
/**
|
||||
* convert ReadableSpan to SpanEntity
|
||||
* @param span ReadableSpan
|
||||
* @returns SpanEntity
|
||||
*/
|
||||
export function convertSpanToSpanEntity(span: ReadableSpan): SpanEntity {
|
||||
return {
|
||||
id: span.spanContext().spanId,
|
||||
traceId: span.spanContext().traceId,
|
||||
parentId: span.parentSpanContext?.spanId || '',
|
||||
name: span.name,
|
||||
startTime: span.startTime[0] * 1e3 + Math.floor(span.startTime[1] / 1e6), // 转为毫秒
|
||||
endTime: span.endTime ? span.endTime[0] * 1e3 + Math.floor(span.endTime[1] / 1e6) : undefined, // 转为毫秒
|
||||
attributes: { ...span.attributes },
|
||||
status: SpanStatusCode[span.status.code],
|
||||
events: span.events,
|
||||
kind: SpanKind[span.kind],
|
||||
links: span.links,
|
||||
modelName: span.attributes?.modelName
|
||||
} as SpanEntity
|
||||
}
|
||||
7
packages/mcp-trace/trace-core/core/traceCache.ts
Normal file
7
packages/mcp-trace/trace-core/core/traceCache.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||
|
||||
export interface TraceCache {
|
||||
createSpan: (span: ReadableSpan) => void
|
||||
endSpan: (span: ReadableSpan) => void
|
||||
clear: () => void
|
||||
}
|
||||
163
packages/mcp-trace/trace-core/core/traceMethod.ts
Normal file
163
packages/mcp-trace/trace-core/core/traceMethod.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SpanStatusCode, trace } from '@opentelemetry/api'
|
||||
import { context as traceContext } from '@opentelemetry/api'
|
||||
|
||||
import { defaultConfig } from '../types/config'
|
||||
|
||||
export interface SpanDecoratorOptions {
|
||||
spanName?: string
|
||||
traceName?: string
|
||||
tag?: string
|
||||
}
|
||||
|
||||
export function TraceMethod(traced: SpanDecoratorOptions) {
|
||||
return function (target: any, propertyKey?: any, descriptor?: PropertyDescriptor | undefined) {
|
||||
// 兼容静态方法装饰器只传2个参数的情况
|
||||
if (!descriptor) {
|
||||
descriptor = Object.getOwnPropertyDescriptor(target, propertyKey)
|
||||
}
|
||||
if (!descriptor || typeof descriptor.value !== 'function') {
|
||||
throw new Error('TraceMethod can only be applied to methods.')
|
||||
}
|
||||
|
||||
const originalMethod = descriptor.value
|
||||
const traceName = traced.traceName || defaultConfig.defaultTracerName || 'default'
|
||||
const tracer = trace.getTracer(traceName)
|
||||
|
||||
descriptor.value = function (...args: any[]) {
|
||||
const name = traced.spanName || propertyKey
|
||||
return tracer.startActiveSpan(name, async (span) => {
|
||||
try {
|
||||
span.setAttribute('inputs', convertToString(args))
|
||||
span.setAttribute('tags', traced.tag || '')
|
||||
const result = await originalMethod.apply(this, args)
|
||||
span.setAttribute('outputs', convertToString(result))
|
||||
span.setStatus({ code: SpanStatusCode.OK })
|
||||
return result
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error))
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: err.message
|
||||
})
|
||||
span.recordException(err)
|
||||
throw error
|
||||
} finally {
|
||||
span.end()
|
||||
}
|
||||
})
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
export function TraceProperty(traced: SpanDecoratorOptions) {
|
||||
return (target: any, propertyKey: string, descriptor?: PropertyDescriptor) => {
|
||||
// 处理箭头函数类属性
|
||||
const traceName = traced.traceName || defaultConfig.defaultTracerName || 'default'
|
||||
const tracer = trace.getTracer(traceName)
|
||||
const name = traced.spanName || propertyKey
|
||||
|
||||
if (!descriptor) {
|
||||
const originalValue = target[propertyKey]
|
||||
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
value: async function (...args: any[]) {
|
||||
const span = tracer.startSpan(name)
|
||||
try {
|
||||
span.setAttribute('inputs', convertToString(args))
|
||||
span.setAttribute('tags', traced.tag || '')
|
||||
const result = await originalValue.apply(this, args)
|
||||
span.setAttribute('outputs', convertToString(result))
|
||||
return result
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error))
|
||||
span.recordException(err)
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message })
|
||||
throw error
|
||||
} finally {
|
||||
span.end()
|
||||
}
|
||||
},
|
||||
configurable: true,
|
||||
writable: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 标准方法装饰器逻辑
|
||||
const originalMethod = descriptor.value
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const span = tracer.startSpan(name)
|
||||
try {
|
||||
span.setAttribute('inputs', convertToString(args))
|
||||
span.setAttribute('tags', traced.tag || '')
|
||||
const result = await originalMethod.apply(this, args)
|
||||
span.setAttribute('outputs', convertToString(result))
|
||||
return result
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error))
|
||||
span.recordException(err)
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message })
|
||||
throw error
|
||||
} finally {
|
||||
span.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function withSpanFunc<F extends (...args: any[]) => any>(
|
||||
name: string,
|
||||
tag: string,
|
||||
fn: F,
|
||||
args: Parameters<F>
|
||||
): ReturnType<F> {
|
||||
const traceName = defaultConfig.defaultTracerName || 'default'
|
||||
const tracer = trace.getTracer(traceName)
|
||||
const _name = name || fn.name || 'anonymousFunction'
|
||||
return traceContext.with(traceContext.active(), () =>
|
||||
tracer.startActiveSpan(
|
||||
_name,
|
||||
{
|
||||
attributes: {
|
||||
tags: tag || '',
|
||||
inputs: JSON.stringify(args)
|
||||
}
|
||||
},
|
||||
(span) => {
|
||||
// 在这里调用原始函数
|
||||
const result = fn(...args)
|
||||
if (result instanceof Promise) {
|
||||
return result
|
||||
.then((res) => {
|
||||
span.setStatus({ code: SpanStatusCode.OK })
|
||||
span.setAttribute('outputs', convertToString(res))
|
||||
return res
|
||||
})
|
||||
.catch((error) => {
|
||||
const err = error instanceof Error ? error : new Error(String(error))
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message })
|
||||
span.recordException(err)
|
||||
throw error
|
||||
})
|
||||
.finally(() => span.end())
|
||||
} else {
|
||||
span.setStatus({ code: SpanStatusCode.OK })
|
||||
span.setAttribute('outputs', convertToString(result))
|
||||
span.end()
|
||||
}
|
||||
return result
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function convertToString(args: any | any[]): string | boolean | number {
|
||||
if (typeof args === 'string' || typeof args === 'boolean' || typeof args === 'number') {
|
||||
return args
|
||||
}
|
||||
return JSON.stringify(args)
|
||||
}
|
||||
26
packages/mcp-trace/trace-core/exporters/FuncSpanExporter.ts
Normal file
26
packages/mcp-trace/trace-core/exporters/FuncSpanExporter.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ExportResult, ExportResultCode } from '@opentelemetry/core'
|
||||
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||
|
||||
export type SaveFunction = (spans: ReadableSpan[]) => Promise<void>
|
||||
|
||||
export class FunctionSpanExporter implements SpanExporter {
|
||||
private exportFunction: SaveFunction
|
||||
|
||||
constructor(fn: SaveFunction) {
|
||||
this.exportFunction = fn
|
||||
}
|
||||
|
||||
shutdown(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
|
||||
this.exportFunction(spans)
|
||||
.then(() => {
|
||||
resultCallback({ code: ExportResultCode.SUCCESS })
|
||||
})
|
||||
.catch((error) => {
|
||||
resultCallback({ code: ExportResultCode.FAILED, error: error })
|
||||
})
|
||||
}
|
||||
}
|
||||
8
packages/mcp-trace/trace-core/index.ts
Normal file
8
packages/mcp-trace/trace-core/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export * from './core/spanConvert'
|
||||
export * from './core/traceCache'
|
||||
export * from './core/traceMethod'
|
||||
export * from './exporters/FuncSpanExporter'
|
||||
export * from './processors/CacheSpanProcessor'
|
||||
export * from './processors/EmitterSpanProcessor'
|
||||
export * from './processors/FuncSpanProcessor'
|
||||
export * from './types/config'
|
||||
@ -0,0 +1,40 @@
|
||||
import { Context, trace } from '@opentelemetry/api'
|
||||
import { BatchSpanProcessor, BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||
|
||||
import { TraceCache } from '../core/traceCache'
|
||||
|
||||
export class CacheBatchSpanProcessor extends BatchSpanProcessor {
|
||||
private cache: TraceCache
|
||||
|
||||
constructor(_exporter: SpanExporter, cache: TraceCache, config?: BufferConfig) {
|
||||
super(_exporter, config)
|
||||
this.cache = cache
|
||||
}
|
||||
|
||||
override onEnd(span: ReadableSpan): void {
|
||||
super.onEnd(span)
|
||||
this.cache.endSpan(span)
|
||||
}
|
||||
|
||||
override onStart(span: Span, parentContext: Context): void {
|
||||
super.onStart(span, parentContext)
|
||||
this.cache.createSpan({
|
||||
name: span.name,
|
||||
kind: span.kind,
|
||||
spanContext: () => span.spanContext(),
|
||||
parentSpanContext: trace.getSpanContext(parentContext),
|
||||
startTime: span.startTime,
|
||||
status: span.status,
|
||||
attributes: span.attributes,
|
||||
links: span.links,
|
||||
events: span.events,
|
||||
duration: span.duration,
|
||||
ended: span.ended,
|
||||
resource: span.resource,
|
||||
instrumentationScope: span.instrumentationScope,
|
||||
droppedAttributesCount: span.droppedAttributesCount,
|
||||
droppedEventsCount: span.droppedEventsCount,
|
||||
droppedLinksCount: span.droppedLinksCount
|
||||
} as ReadableSpan)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { Context } from '@opentelemetry/api'
|
||||
import { BatchSpanProcessor, BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||
import { EventEmitter } from 'stream'
|
||||
|
||||
import { convertSpanToSpanEntity } from '../core/spanConvert'
|
||||
|
||||
export const TRACE_DATA_EVENT = 'trace_data_event'
|
||||
export const ON_START = 'start'
|
||||
export const ON_END = 'end'
|
||||
|
||||
export class EmitterSpanProcessor extends BatchSpanProcessor {
|
||||
private emitter: EventEmitter
|
||||
|
||||
constructor(_exporter: SpanExporter, emitter: NodeJS.EventEmitter, config?: BufferConfig) {
|
||||
super(_exporter, config)
|
||||
this.emitter = emitter
|
||||
}
|
||||
|
||||
override onEnd(span: ReadableSpan): void {
|
||||
super.onEnd(span)
|
||||
this.emitter.emit(TRACE_DATA_EVENT, ON_END, convertSpanToSpanEntity(span))
|
||||
}
|
||||
|
||||
override onStart(span: Span, parentContext: Context): void {
|
||||
super.onStart(span, parentContext)
|
||||
this.emitter.emit(TRACE_DATA_EVENT, ON_START, convertSpanToSpanEntity(span))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { Context, trace } from '@opentelemetry/api'
|
||||
import { BatchSpanProcessor, BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||
|
||||
export type SpanFunction = (span: ReadableSpan) => void
|
||||
|
||||
export class FunctionSpanProcessor extends BatchSpanProcessor {
|
||||
private start: SpanFunction
|
||||
private end: SpanFunction
|
||||
|
||||
constructor(_exporter: SpanExporter, start: SpanFunction, end: SpanFunction, config?: BufferConfig) {
|
||||
super(_exporter, config)
|
||||
this.start = start
|
||||
this.end = end
|
||||
}
|
||||
|
||||
override onEnd(span: ReadableSpan): void {
|
||||
super.onEnd(span)
|
||||
this.end(span)
|
||||
}
|
||||
|
||||
override onStart(span: Span, parentContext: Context): void {
|
||||
super.onStart(span, parentContext)
|
||||
this.start({
|
||||
name: span.name,
|
||||
kind: span.kind,
|
||||
spanContext: () => span.spanContext(),
|
||||
parentSpanContext: trace.getSpanContext(parentContext),
|
||||
startTime: span.startTime,
|
||||
status: span.status,
|
||||
attributes: span.attributes,
|
||||
links: span.links,
|
||||
events: span.events,
|
||||
duration: span.duration,
|
||||
ended: span.ended,
|
||||
resource: span.resource,
|
||||
instrumentationScope: span.instrumentationScope,
|
||||
droppedAttributesCount: span.droppedAttributesCount,
|
||||
droppedEventsCount: span.droppedEventsCount,
|
||||
droppedLinksCount: span.droppedLinksCount
|
||||
} as ReadableSpan)
|
||||
}
|
||||
}
|
||||
67
packages/mcp-trace/trace-core/types/config.ts
Normal file
67
packages/mcp-trace/trace-core/types/config.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Link } from '@opentelemetry/api'
|
||||
import { TimedEvent } from '@opentelemetry/sdk-trace-base'
|
||||
|
||||
export type AttributeValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<null | undefined | string>
|
||||
| Array<null | undefined | number>
|
||||
| Array<null | undefined | boolean>
|
||||
| { [key: string]: string | number | boolean }
|
||||
| Array<null | undefined | { [key: string]: string | number | boolean }>
|
||||
|
||||
export type Attributes = {
|
||||
[key: string]: AttributeValue
|
||||
}
|
||||
|
||||
export interface TelemetryConfig {
|
||||
serviceName: string
|
||||
endpoint?: string
|
||||
headers?: Record<string, string>
|
||||
defaultTracerName?: string
|
||||
isDevModel?: boolean
|
||||
}
|
||||
|
||||
export interface TraceConfig extends TelemetryConfig {
|
||||
maxAttributesPerSpan?: number
|
||||
}
|
||||
|
||||
export interface TraceEntity {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface TokenUsage {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
total_tokens: number
|
||||
prompt_tokens_details?: {
|
||||
[key: string]: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface SpanEntity {
|
||||
id: string
|
||||
name: string
|
||||
parentId: string
|
||||
traceId: string
|
||||
status: string
|
||||
kind: string
|
||||
attributes: Attributes | undefined
|
||||
isEnd: boolean
|
||||
events: TimedEvent[] | undefined
|
||||
startTime: number
|
||||
endTime: number | null
|
||||
links: Link[] | undefined
|
||||
topicId?: string
|
||||
usage?: TokenUsage
|
||||
modelName?: string
|
||||
}
|
||||
|
||||
export const defaultConfig: TelemetryConfig = {
|
||||
serviceName: 'default',
|
||||
headers: {},
|
||||
defaultTracerName: 'default',
|
||||
isDevModel: true
|
||||
}
|
||||
46
packages/mcp-trace/trace-node/nodeTracer.ts
Normal file
46
packages/mcp-trace/trace-node/nodeTracer.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { trace, Tracer } from '@opentelemetry/api'
|
||||
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
|
||||
import { W3CTraceContextPropagator } from '@opentelemetry/core'
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
||||
import { BatchSpanProcessor, ConsoleSpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base'
|
||||
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
|
||||
|
||||
import { defaultConfig, TraceConfig } from '../trace-core/types/config'
|
||||
|
||||
export class NodeTracer {
|
||||
private static provider: NodeTracerProvider
|
||||
private static defaultTracer: Tracer
|
||||
private static spanProcessor: SpanProcessor
|
||||
|
||||
static init(config?: TraceConfig, spanProcessor?: SpanProcessor) {
|
||||
if (config) {
|
||||
defaultConfig.serviceName = config.serviceName || defaultConfig.serviceName
|
||||
defaultConfig.endpoint = config.endpoint || defaultConfig.endpoint
|
||||
defaultConfig.headers = config.headers || defaultConfig.headers
|
||||
defaultConfig.defaultTracerName = config.defaultTracerName || defaultConfig.defaultTracerName
|
||||
}
|
||||
this.spanProcessor = spanProcessor || new BatchSpanProcessor(this.getExporter())
|
||||
this.provider = new NodeTracerProvider({
|
||||
spanProcessors: [this.spanProcessor]
|
||||
})
|
||||
this.provider.register({
|
||||
propagator: new W3CTraceContextPropagator(),
|
||||
contextManager: new AsyncLocalStorageContextManager()
|
||||
})
|
||||
this.defaultTracer = trace.getTracer(config?.defaultTracerName || 'default')
|
||||
}
|
||||
|
||||
private static getExporter(config?: TraceConfig) {
|
||||
if (config && config.endpoint) {
|
||||
return new OTLPTraceExporter({
|
||||
url: `${config.endpoint}/v1/traces`,
|
||||
headers: config.headers || undefined
|
||||
})
|
||||
}
|
||||
return new ConsoleSpanExporter()
|
||||
}
|
||||
|
||||
public static getTracer() {
|
||||
return this.defaultTracer
|
||||
}
|
||||
}
|
||||
75
packages/mcp-trace/trace-web/TopicContextManager.ts
Normal file
75
packages/mcp-trace/trace-web/TopicContextManager.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Context, ContextManager, ROOT_CONTEXT } from '@opentelemetry/api'
|
||||
|
||||
export class TopicContextManager implements ContextManager {
|
||||
private topicContextStack: Map<string, Context[]>
|
||||
private _topicContexts: Map<string, Context>
|
||||
|
||||
constructor() {
|
||||
// topicId -> context
|
||||
this.topicContextStack = new Map()
|
||||
this._topicContexts = new Map()
|
||||
}
|
||||
|
||||
// 绑定一个context到topicId
|
||||
startContextForTopic(topicId, context: Context) {
|
||||
const currentContext = this.getCurrentContext(topicId)
|
||||
this._topicContexts.set(topicId, context)
|
||||
if (!this.topicContextStack.has(topicId) && !this.topicContextStack.get(topicId)) {
|
||||
this.topicContextStack.set(topicId, [currentContext])
|
||||
} else {
|
||||
this.topicContextStack.get(topicId)?.push(currentContext)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取topicId对应的context
|
||||
getContextForTopic(topicId) {
|
||||
return this.getCurrentContext(topicId)
|
||||
}
|
||||
|
||||
endContextForTopic(topicId) {
|
||||
const context = this.getHistoryContext(topicId)
|
||||
this._topicContexts.set(topicId, context)
|
||||
}
|
||||
|
||||
cleanContextForTopic(topicId) {
|
||||
this.topicContextStack.delete(topicId)
|
||||
this._topicContexts.delete(topicId)
|
||||
}
|
||||
|
||||
private getHistoryContext(topicId): Context {
|
||||
const hasContext = this.topicContextStack.has(topicId) && this.topicContextStack.get(topicId)
|
||||
const context = hasContext && hasContext.length > 0 && hasContext.pop()
|
||||
return context ? context : ROOT_CONTEXT
|
||||
}
|
||||
|
||||
private getCurrentContext(topicId): Context {
|
||||
const hasContext = this._topicContexts.has(topicId) && this._topicContexts.get(topicId)
|
||||
return hasContext || ROOT_CONTEXT
|
||||
}
|
||||
|
||||
// OpenTelemetry接口实现
|
||||
active() {
|
||||
// 不支持全局active,必须显式传递
|
||||
return ROOT_CONTEXT
|
||||
}
|
||||
|
||||
with(_, fn, thisArg, ...args) {
|
||||
// 直接调用fn,不做全局active切换
|
||||
return fn.apply(thisArg, args)
|
||||
}
|
||||
|
||||
bind(target, context) {
|
||||
// 显式绑定
|
||||
target.__ot_context = context
|
||||
return target
|
||||
}
|
||||
|
||||
enable() {
|
||||
return this
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._topicContexts.clear()
|
||||
return this
|
||||
}
|
||||
}
|
||||
3
packages/mcp-trace/trace-web/index.ts
Normal file
3
packages/mcp-trace/trace-web/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './TopicContextManager'
|
||||
export * from './traceContextPromise'
|
||||
export * from './webTracer'
|
||||
99
packages/mcp-trace/trace-web/traceContextPromise.ts
Normal file
99
packages/mcp-trace/trace-web/traceContextPromise.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { Context, context } from '@opentelemetry/api'
|
||||
|
||||
const originalPromise = globalThis.Promise
|
||||
|
||||
class TraceContextPromise<T> extends Promise<T> {
|
||||
_context: Context
|
||||
|
||||
constructor(
|
||||
executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void,
|
||||
ctx?: Context
|
||||
) {
|
||||
const capturedContext = ctx || context.active()
|
||||
super((resolve, reject) => {
|
||||
context.with(capturedContext, () => {
|
||||
executor(
|
||||
(value) => context.with(capturedContext, () => resolve(value)),
|
||||
(reason) => context.with(capturedContext, () => reject(reason))
|
||||
)
|
||||
})
|
||||
})
|
||||
this._context = capturedContext
|
||||
}
|
||||
|
||||
// 兼容 Promise.resolve/reject
|
||||
static resolve(): Promise<void>
|
||||
static resolve<T>(value: T | PromiseLike<T>): Promise<T>
|
||||
static resolve<T>(value: T | PromiseLike<T>, ctx?: Context): Promise<T>
|
||||
static resolve<T>(value?: T | PromiseLike<T>, ctx?: Context): Promise<T | void> {
|
||||
return new TraceContextPromise<T | void>((resolve) => resolve(value as T), ctx)
|
||||
}
|
||||
|
||||
static reject<T = never>(reason?: any): Promise<T>
|
||||
static reject<T = never>(reason?: any, ctx?: Context): Promise<T> {
|
||||
return new TraceContextPromise<T>((_, reject) => reject(reason), ctx)
|
||||
}
|
||||
|
||||
static all<T>(values: (T | PromiseLike<T>)[]): Promise<T[]> {
|
||||
// 尝试从缓存获取 context
|
||||
let capturedContext = context.active()
|
||||
const newValues = values.map((v) => {
|
||||
if (v instanceof Promise && !(v instanceof TraceContextPromise)) {
|
||||
return new TraceContextPromise((resolve, reject) => v.then(resolve, reject), capturedContext)
|
||||
} else if (typeof v === 'function') {
|
||||
// 如果 v 是一个 Function,使用 context 传递 trace 上下文
|
||||
return (...args: any[]) => context.with(capturedContext, () => v(...args))
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
})
|
||||
if (Array.isArray(values) && values.length > 0 && values[0] instanceof TraceContextPromise) {
|
||||
capturedContext = (values[0] as TraceContextPromise<any>)._context
|
||||
}
|
||||
return originalPromise.all(newValues) as Promise<T[]>
|
||||
}
|
||||
|
||||
static race<T>(values: (T | PromiseLike<T>)[]): Promise<T> {
|
||||
const capturedContext = context.active()
|
||||
return new TraceContextPromise<T>((resolve, reject) => {
|
||||
originalPromise.race(values).then(
|
||||
(result) => context.with(capturedContext, () => resolve(result)),
|
||||
(err) => context.with(capturedContext, () => reject(err))
|
||||
)
|
||||
}, capturedContext)
|
||||
}
|
||||
|
||||
static allSettled<T>(values: (T | PromiseLike<T>)[]): Promise<PromiseSettledResult<T>[]> {
|
||||
const capturedContext = context.active()
|
||||
return new TraceContextPromise<PromiseSettledResult<T>[]>((resolve, reject) => {
|
||||
originalPromise.allSettled(values).then(
|
||||
(result) => context.with(capturedContext, () => resolve(result)),
|
||||
(err) => context.with(capturedContext, () => reject(err))
|
||||
)
|
||||
}, capturedContext)
|
||||
}
|
||||
|
||||
static any<T>(values: (T | PromiseLike<T>)[]): Promise<T> {
|
||||
const capturedContext = context.active()
|
||||
return new TraceContextPromise<T>((resolve, reject) => {
|
||||
originalPromise.any(values).then(
|
||||
(result) => context.with(capturedContext, () => resolve(result)),
|
||||
(err) => context.with(capturedContext, () => reject(err))
|
||||
)
|
||||
}, capturedContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 TraceContextPromise 替换全局 Promise
|
||||
*/
|
||||
export function instrumentPromises() {
|
||||
globalThis.Promise = TraceContextPromise as unknown as PromiseConstructor
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复原生 Promise
|
||||
*/
|
||||
export function uninstrumentPromises() {
|
||||
globalThis.Promise = originalPromise
|
||||
}
|
||||
46
packages/mcp-trace/trace-web/webTracer.ts
Normal file
46
packages/mcp-trace/trace-web/webTracer.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { W3CTraceContextPropagator } from '@opentelemetry/core'
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
||||
import { BatchSpanProcessor, ConsoleSpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base'
|
||||
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
|
||||
|
||||
import { defaultConfig, TraceConfig } from '../trace-core/types/config'
|
||||
import { TopicContextManager } from './TopicContextManager'
|
||||
|
||||
export const contextManager = new TopicContextManager()
|
||||
|
||||
export class WebTracer {
|
||||
private static provider: WebTracerProvider
|
||||
private static processor: SpanProcessor
|
||||
|
||||
static init(config?: TraceConfig, spanProcessor?: SpanProcessor) {
|
||||
if (config) {
|
||||
defaultConfig.serviceName = config.serviceName || defaultConfig.serviceName
|
||||
defaultConfig.endpoint = config.endpoint || defaultConfig.endpoint
|
||||
defaultConfig.headers = config.headers || defaultConfig.headers
|
||||
defaultConfig.defaultTracerName = config.defaultTracerName || defaultConfig.defaultTracerName
|
||||
}
|
||||
this.processor = spanProcessor || new BatchSpanProcessor(this.getExporter())
|
||||
this.provider = new WebTracerProvider({
|
||||
spanProcessors: [this.processor]
|
||||
})
|
||||
this.provider.register({
|
||||
propagator: new W3CTraceContextPropagator(),
|
||||
contextManager: contextManager
|
||||
})
|
||||
}
|
||||
|
||||
private static getExporter() {
|
||||
if (defaultConfig.endpoint) {
|
||||
return new OTLPTraceExporter({
|
||||
url: `${defaultConfig.endpoint}/v1/traces`,
|
||||
headers: defaultConfig.headers
|
||||
})
|
||||
}
|
||||
return new ConsoleSpanExporter()
|
||||
}
|
||||
}
|
||||
|
||||
export const startContext = contextManager.startContextForTopic.bind(contextManager)
|
||||
export const getContext = contextManager.getContextForTopic.bind(contextManager)
|
||||
export const endContext = contextManager.endContextForTopic.bind(contextManager)
|
||||
export const cleanContext = contextManager.cleanContextForTopic.bind(contextManager)
|
||||
@ -257,5 +257,20 @@ export enum IpcChannel {
|
||||
Memory_SetConfig = 'memory:set-config',
|
||||
Memory_DeleteUser = 'memory:delete-user',
|
||||
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
|
||||
Memory_GetUsersList = 'memory:get-users-list'
|
||||
Memory_GetUsersList = 'memory:get-users-list',
|
||||
|
||||
// TRACE
|
||||
TRACE_SAVE_DATA = 'trace:saveData',
|
||||
TRACE_GET_DATA = 'trace:getData',
|
||||
TRACE_SAVE_ENTITY = 'trace:saveEntity',
|
||||
TRACE_GET_ENTITY = 'trace:getEntity',
|
||||
TRACE_BIND_TOPIC = 'trace:bindTopic',
|
||||
TRACE_CLEAN_TOPIC = 'trace:cleanTopic',
|
||||
TRACE_TOKEN_USAGE = 'trace:tokenUsage',
|
||||
TRACE_CLEAN_HISTORY = 'trace:cleanHistory',
|
||||
TRACE_OPEN_WINDOW = 'trace:openWindow',
|
||||
TRACE_SET_TITLE = 'trace:setTitle',
|
||||
TRACE_ADD_END_MESSAGE = 'trace:addEndMessage',
|
||||
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
|
||||
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage'
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import { isDev, isLinux, isWin } from './constant'
|
||||
import { registerIpc } from './ipc'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import mcpService from './services/MCPService'
|
||||
import { nodeTraceService } from './services/NodeTraceService'
|
||||
import {
|
||||
CHERRY_STUDIO_PROTOCOL,
|
||||
handleProtocolUrl,
|
||||
@ -109,6 +110,8 @@ if (!app.requestSingleInstanceLock()) {
|
||||
const mainWindow = windowService.createMainWindow()
|
||||
new TrayService()
|
||||
|
||||
nodeTraceService.init()
|
||||
|
||||
app.on('activate', function () {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
|
||||
158
src/main/ipc.ts
158
src/main/ipc.ts
@ -6,6 +6,7 @@ import { loggerService } from '@logger'
|
||||
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { handleZoomFactor } from '@main/utils/zoom'
|
||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { UpgradeChannel } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
|
||||
@ -24,6 +25,7 @@ import FileService from './services/FileSystemService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import mcpService from './services/MCPService'
|
||||
import MemoryService from './services/memory/MemoryService'
|
||||
import { openTraceWindow, setTraceWindowTitle } from './services/NodeTraceService'
|
||||
import NotificationService from './services/NotificationService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
@ -33,6 +35,19 @@ import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||
import { searchService } from './services/SearchService'
|
||||
import { SelectionService } from './services/SelectionService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import {
|
||||
addEndMessage,
|
||||
addStreamMessage,
|
||||
bindTopic,
|
||||
cleanHistoryTrace,
|
||||
cleanLocalData,
|
||||
cleanTopic,
|
||||
getEntity,
|
||||
getSpans,
|
||||
saveEntity,
|
||||
saveSpans,
|
||||
tokenUsage
|
||||
} from './services/SpanCacheService'
|
||||
import storeSyncService from './services/StoreSyncService'
|
||||
import { themeService } from './services/ThemeService'
|
||||
import VertexAIService from './services/VertexAIService'
|
||||
@ -371,49 +386,49 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
|
||||
// backup
|
||||
ipcMain.handle(IpcChannel.Backup_Backup, backupManager.backup)
|
||||
ipcMain.handle(IpcChannel.Backup_Restore, backupManager.restore)
|
||||
ipcMain.handle(IpcChannel.Backup_BackupToWebdav, backupManager.backupToWebdav)
|
||||
ipcMain.handle(IpcChannel.Backup_RestoreFromWebdav, backupManager.restoreFromWebdav)
|
||||
ipcMain.handle(IpcChannel.Backup_ListWebdavFiles, backupManager.listWebdavFiles)
|
||||
ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection)
|
||||
ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory)
|
||||
ipcMain.handle(IpcChannel.Backup_DeleteWebdavFile, backupManager.deleteWebdavFile)
|
||||
ipcMain.handle(IpcChannel.Backup_BackupToLocalDir, backupManager.backupToLocalDir)
|
||||
ipcMain.handle(IpcChannel.Backup_RestoreFromLocalBackup, backupManager.restoreFromLocalBackup)
|
||||
ipcMain.handle(IpcChannel.Backup_ListLocalBackupFiles, backupManager.listLocalBackupFiles)
|
||||
ipcMain.handle(IpcChannel.Backup_DeleteLocalBackupFile, backupManager.deleteLocalBackupFile)
|
||||
ipcMain.handle(IpcChannel.Backup_SetLocalBackupDir, backupManager.setLocalBackupDir)
|
||||
ipcMain.handle(IpcChannel.Backup_BackupToS3, backupManager.backupToS3)
|
||||
ipcMain.handle(IpcChannel.Backup_RestoreFromS3, backupManager.restoreFromS3)
|
||||
ipcMain.handle(IpcChannel.Backup_ListS3Files, backupManager.listS3Files)
|
||||
ipcMain.handle(IpcChannel.Backup_DeleteS3File, backupManager.deleteS3File)
|
||||
ipcMain.handle(IpcChannel.Backup_CheckS3Connection, backupManager.checkS3Connection)
|
||||
ipcMain.handle(IpcChannel.Backup_Backup, backupManager.backup.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_Restore, backupManager.restore.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_BackupToWebdav, backupManager.backupToWebdav.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_RestoreFromWebdav, backupManager.restoreFromWebdav.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_ListWebdavFiles, backupManager.listWebdavFiles.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_DeleteWebdavFile, backupManager.deleteWebdavFile.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_BackupToLocalDir, backupManager.backupToLocalDir.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_RestoreFromLocalBackup, backupManager.restoreFromLocalBackup.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_ListLocalBackupFiles, backupManager.listLocalBackupFiles.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_DeleteLocalBackupFile, backupManager.deleteLocalBackupFile.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_SetLocalBackupDir, backupManager.setLocalBackupDir.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_BackupToS3, backupManager.backupToS3.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_RestoreFromS3, backupManager.restoreFromS3.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_ListS3Files, backupManager.listS3Files.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_DeleteS3File, backupManager.deleteS3File.bind(backupManager))
|
||||
ipcMain.handle(IpcChannel.Backup_CheckS3Connection, backupManager.checkS3Connection.bind(backupManager))
|
||||
|
||||
// file
|
||||
ipcMain.handle(IpcChannel.File_Open, fileManager.open)
|
||||
ipcMain.handle(IpcChannel.File_OpenPath, fileManager.openPath)
|
||||
ipcMain.handle(IpcChannel.File_Save, fileManager.save)
|
||||
ipcMain.handle(IpcChannel.File_Select, fileManager.selectFile)
|
||||
ipcMain.handle(IpcChannel.File_Upload, fileManager.uploadFile)
|
||||
ipcMain.handle(IpcChannel.File_Clear, fileManager.clear)
|
||||
ipcMain.handle(IpcChannel.File_Read, fileManager.readFile)
|
||||
ipcMain.handle(IpcChannel.File_Delete, fileManager.deleteFile)
|
||||
ipcMain.handle('file:deleteDir', fileManager.deleteDir)
|
||||
ipcMain.handle(IpcChannel.File_Get, fileManager.getFile)
|
||||
ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder)
|
||||
ipcMain.handle(IpcChannel.File_CreateTempFile, fileManager.createTempFile)
|
||||
ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile)
|
||||
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId)
|
||||
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
|
||||
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
|
||||
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image)
|
||||
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File)
|
||||
ipcMain.handle(IpcChannel.File_GetPdfInfo, fileManager.pdfPageCount)
|
||||
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
|
||||
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
|
||||
ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage)
|
||||
ipcMain.handle(IpcChannel.File_OpenWithRelativePath, fileManager.openFileWithRelativePath)
|
||||
ipcMain.handle(IpcChannel.File_Open, fileManager.open.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_OpenPath, fileManager.openPath.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Save, fileManager.save.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Select, fileManager.selectFile.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Upload, fileManager.uploadFile.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Clear, fileManager.clear.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Read, fileManager.readFile.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Delete, fileManager.deleteFile.bind(fileManager))
|
||||
ipcMain.handle('file:deleteDir', fileManager.deleteDir.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Get, fileManager.getFile.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_CreateTempFile, fileManager.createTempFile.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_GetPdfInfo, fileManager.pdfPageCount.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_OpenWithRelativePath, fileManager.openFileWithRelativePath.bind(fileManager))
|
||||
|
||||
// file service
|
||||
ipcMain.handle(IpcChannel.FileService_Upload, async (_, provider: Provider, file: FileMetadata) => {
|
||||
@ -437,10 +452,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
|
||||
// fs
|
||||
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile)
|
||||
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile.bind(FileService))
|
||||
|
||||
// export
|
||||
ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord)
|
||||
ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord.bind(exportService))
|
||||
|
||||
// open path
|
||||
ipcMain.handle(IpcChannel.Open_Path, async (_, path: string) => {
|
||||
@ -458,14 +473,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
|
||||
// knowledge base
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Reset, KnowledgeService.reset)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Delete, KnowledgeService.delete)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Add, KnowledgeService.add)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Remove, KnowledgeService.remove)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Search, KnowledgeService.search)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Rerank, KnowledgeService.rerank)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Check_Quota, KnowledgeService.checkQuota)
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create.bind(KnowledgeService))
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Reset, KnowledgeService.reset.bind(KnowledgeService))
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Delete, KnowledgeService.delete.bind(KnowledgeService))
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Add, KnowledgeService.add.bind(KnowledgeService))
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Remove, KnowledgeService.remove.bind(KnowledgeService))
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Search, KnowledgeService.search.bind(KnowledgeService))
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Rerank, KnowledgeService.rerank.bind(KnowledgeService))
|
||||
ipcMain.handle(IpcChannel.KnowledgeBase_Check_Quota, KnowledgeService.checkQuota.bind(KnowledgeService))
|
||||
|
||||
// memory
|
||||
ipcMain.handle(IpcChannel.Memory_Add, async (_, messages, config) => {
|
||||
@ -586,12 +601,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.App_InstallBunBinary, () => runInstallScript('install-bun.js'))
|
||||
|
||||
//copilot
|
||||
ipcMain.handle(IpcChannel.Copilot_GetAuthMessage, CopilotService.getAuthMessage)
|
||||
ipcMain.handle(IpcChannel.Copilot_GetCopilotToken, CopilotService.getCopilotToken)
|
||||
ipcMain.handle(IpcChannel.Copilot_SaveCopilotToken, CopilotService.saveCopilotToken)
|
||||
ipcMain.handle(IpcChannel.Copilot_GetToken, CopilotService.getToken)
|
||||
ipcMain.handle(IpcChannel.Copilot_Logout, CopilotService.logout)
|
||||
ipcMain.handle(IpcChannel.Copilot_GetUser, CopilotService.getUser)
|
||||
ipcMain.handle(IpcChannel.Copilot_GetAuthMessage, CopilotService.getAuthMessage.bind(CopilotService))
|
||||
ipcMain.handle(IpcChannel.Copilot_GetCopilotToken, CopilotService.getCopilotToken.bind(CopilotService))
|
||||
ipcMain.handle(IpcChannel.Copilot_SaveCopilotToken, CopilotService.saveCopilotToken.bind(CopilotService))
|
||||
ipcMain.handle(IpcChannel.Copilot_GetToken, CopilotService.getToken.bind(CopilotService))
|
||||
ipcMain.handle(IpcChannel.Copilot_Logout, CopilotService.logout.bind(CopilotService))
|
||||
ipcMain.handle(IpcChannel.Copilot_GetUser, CopilotService.getUser.bind(CopilotService))
|
||||
|
||||
// Obsidian service
|
||||
ipcMain.handle(IpcChannel.Obsidian_GetVaults, () => {
|
||||
@ -603,7 +618,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
|
||||
// nutstore
|
||||
ipcMain.handle(IpcChannel.Nutstore_GetSsoUrl, NutstoreService.getNutstoreSSOUrl)
|
||||
ipcMain.handle(IpcChannel.Nutstore_GetSsoUrl, NutstoreService.getNutstoreSSOUrl.bind(NutstoreService))
|
||||
ipcMain.handle(IpcChannel.Nutstore_DecryptToken, (_, token: string) => NutstoreService.decryptToken(token))
|
||||
ipcMain.handle(IpcChannel.Nutstore_GetDirectoryContents, (_, token: string, path: string) =>
|
||||
NutstoreService.getDirectoryContents(token, path)
|
||||
@ -642,4 +657,31 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => {
|
||||
configManager.setDisableHardwareAcceleration(isDisable)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.TRACE_SAVE_DATA, (_, topicId: string) => saveSpans(topicId))
|
||||
ipcMain.handle(IpcChannel.TRACE_GET_DATA, (_, topicId: string, traceId: string, modelName?: string) =>
|
||||
getSpans(topicId, traceId, modelName)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.TRACE_SAVE_ENTITY, (_, entity: SpanEntity) => saveEntity(entity))
|
||||
ipcMain.handle(IpcChannel.TRACE_GET_ENTITY, (_, spanId: string) => getEntity(spanId))
|
||||
ipcMain.handle(IpcChannel.TRACE_BIND_TOPIC, (_, topicId: string, traceId: string) => bindTopic(traceId, topicId))
|
||||
ipcMain.handle(IpcChannel.TRACE_CLEAN_TOPIC, (_, topicId: string, traceId?: string) => cleanTopic(topicId, traceId))
|
||||
ipcMain.handle(IpcChannel.TRACE_TOKEN_USAGE, (_, spanId: string, usage: TokenUsage) => tokenUsage(spanId, usage))
|
||||
ipcMain.handle(IpcChannel.TRACE_CLEAN_HISTORY, (_, topicId: string, traceId: string, modelName?: string) =>
|
||||
cleanHistoryTrace(topicId, traceId, modelName)
|
||||
)
|
||||
ipcMain.handle(
|
||||
IpcChannel.TRACE_OPEN_WINDOW,
|
||||
(_, topicId: string, traceId: string, autoOpen?: boolean, modelName?: string) =>
|
||||
openTraceWindow(topicId, traceId, autoOpen, modelName)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.TRACE_SET_TITLE, (_, title: string) => setTraceWindowTitle(title))
|
||||
ipcMain.handle(IpcChannel.TRACE_ADD_END_MESSAGE, (_, spanId: string, modelName: string, message: string) =>
|
||||
addEndMessage(spanId, modelName, message)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.TRACE_CLEAN_LOCAL_DATA, () => cleanLocalData())
|
||||
ipcMain.handle(
|
||||
IpcChannel.TRACE_ADD_STREAM_MESSAGE,
|
||||
(_, spanId: string, modelName: string, context: string, msg: any) =>
|
||||
addStreamMessage(spanId, modelName, context, msg)
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { TraceMethod } from '@mcp-trace/trace-core'
|
||||
import { ApiClient } from '@types'
|
||||
|
||||
import EmbeddingsFactory from './EmbeddingsFactory'
|
||||
@ -14,13 +15,18 @@ export default class Embeddings {
|
||||
public async init(): Promise<void> {
|
||||
return this.sdk.init()
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'dimensions', tag: 'Embeddings' })
|
||||
public async getDimensions(): Promise<number> {
|
||||
return this.sdk.getDimensions()
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'embedDocuments', tag: 'Embeddings' })
|
||||
public async embedDocuments(texts: string[]): Promise<number[][]> {
|
||||
return this.sdk.embedDocuments(texts)
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'embedQuery', tag: 'Embeddings' })
|
||||
public async embedQuery(text: string): Promise<number[]> {
|
||||
return this.sdk.embedQuery(text)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { getConfigDir } from '@main/utils/file'
|
||||
import { TraceMethod } from '@mcp-trace/trace-core'
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { Mutex } from 'async-mutex' // 引入 Mutex
|
||||
@ -45,6 +46,7 @@ class KnowledgeGraphManager {
|
||||
}
|
||||
|
||||
// Static async factory method for initialization
|
||||
@TraceMethod({ spanName: 'create', tag: 'KnowledgeGraph' })
|
||||
public static async create(memoryPath: string): Promise<KnowledgeGraphManager> {
|
||||
const manager = new KnowledgeGraphManager(memoryPath)
|
||||
await manager._ensureMemoryPathExists()
|
||||
@ -143,6 +145,7 @@ class KnowledgeGraphManager {
|
||||
return JSON.parse(relationStr) as Relation
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'createEntities', tag: 'KnowledgeGraph' })
|
||||
async createEntities(entities: Entity[]): Promise<Entity[]> {
|
||||
const newEntities: Entity[] = []
|
||||
entities.forEach((entity) => {
|
||||
@ -159,6 +162,7 @@ class KnowledgeGraphManager {
|
||||
return newEntities
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'createRelations', tag: 'KnowledgeGraph' })
|
||||
async createRelations(relations: Relation[]): Promise<Relation[]> {
|
||||
const newRelations: Relation[] = []
|
||||
relations.forEach((relation) => {
|
||||
@ -179,6 +183,7 @@ class KnowledgeGraphManager {
|
||||
return newRelations
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'addObservtions', tag: 'KnowledgeGraph' })
|
||||
async addObservations(
|
||||
observations: { entityName: string; contents: string[] }[]
|
||||
): Promise<{ entityName: string; addedObservations: string[] }[]> {
|
||||
@ -213,6 +218,7 @@ class KnowledgeGraphManager {
|
||||
return results
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'deleteEntities', tag: 'KnowledgeGraph' })
|
||||
async deleteEntities(entityNames: string[]): Promise<void> {
|
||||
let changed = false
|
||||
const namesToDelete = new Set(entityNames)
|
||||
@ -244,6 +250,7 @@ class KnowledgeGraphManager {
|
||||
}
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'deleteObservations', tag: 'KnowledgeGraph' })
|
||||
async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
|
||||
let changed = false
|
||||
deletions.forEach((d) => {
|
||||
@ -262,6 +269,7 @@ class KnowledgeGraphManager {
|
||||
}
|
||||
}
|
||||
|
||||
@TraceMethod({ spanName: 'deleteRelations', tag: 'KnowledgeGraph' })
|
||||
async deleteRelations(relations: Relation[]): Promise<void> {
|
||||
let changed = false
|
||||
relations.forEach((rel) => {
|
||||
@ -276,6 +284,7 @@ class KnowledgeGraphManager {
|
||||
}
|
||||
|
||||
// Read the current state from memory
|
||||
@TraceMethod({ spanName: 'readGraph', tag: 'KnowledgeGraph' })
|
||||
async readGraph(): Promise<KnowledgeGraph> {
|
||||
// Return a deep copy to prevent external modification of the internal state
|
||||
return JSON.parse(
|
||||
@ -287,6 +296,7 @@ class KnowledgeGraphManager {
|
||||
}
|
||||
|
||||
// Search operates on the in-memory graph
|
||||
@TraceMethod({ spanName: 'searchNodes', tag: 'KnowledgeGraph' })
|
||||
async searchNodes(query: string): Promise<KnowledgeGraph> {
|
||||
const lowerCaseQuery = query.toLowerCase()
|
||||
const filteredEntities = Array.from(this.entities.values()).filter(
|
||||
@ -309,6 +319,7 @@ class KnowledgeGraphManager {
|
||||
}
|
||||
|
||||
// Open operates on the in-memory graph
|
||||
@TraceMethod({ spanName: 'openNodes', tag: 'KnowledgeGraph' })
|
||||
async openNodes(names: string[]): Promise<KnowledgeGraph> {
|
||||
const nameSet = new Set(names)
|
||||
const filteredEntities = Array.from(this.entities.values()).filter((e) => nameSet.has(e.name))
|
||||
|
||||
@ -45,6 +45,7 @@ class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
// @TraceProperty({ spanName: 'getFileHash', tag: 'FileStorage' })
|
||||
private getFileHash = async (filePath: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('md5')
|
||||
@ -219,6 +220,7 @@ class FileStorage {
|
||||
return fileInfo
|
||||
}
|
||||
|
||||
// @TraceProperty({ spanName: 'deleteFile', tag: 'FileStorage' })
|
||||
public deleteFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
|
||||
if (!fs.existsSync(path.join(this.storageDir, id))) {
|
||||
return
|
||||
@ -586,6 +588,7 @@ class FileStorage {
|
||||
return mimeToExtension[mimeType] || '.bin'
|
||||
}
|
||||
|
||||
// @TraceProperty({ spanName: 'copyFile', tag: 'FileStorage' })
|
||||
public copyFile = async (_: Electron.IpcMainInvokeEvent, id: string, destPath: string): Promise<void> => {
|
||||
try {
|
||||
const sourcePath = path.join(this.storageDir, id)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { TraceMethod } from '@mcp-trace/trace-core'
|
||||
import fs from 'fs/promises'
|
||||
|
||||
export default class FileService {
|
||||
@TraceMethod({ spanName: 'readFile', tag: 'FileService' })
|
||||
public static async readFile(_: Electron.IpcMainInvokeEvent, pathOrUrl: string, encoding?: BufferEncoding) {
|
||||
const path = pathOrUrl.startsWith('file://') ? new URL(pathOrUrl) : pathOrUrl
|
||||
if (encoding) return fs.readFile(path, { encoding })
|
||||
|
||||
@ -31,6 +31,7 @@ import Reranker from '@main/knowledge/reranker/Reranker'
|
||||
import { windowService } from '@main/services/WindowService'
|
||||
import { getDataPath } from '@main/utils'
|
||||
import { getAllFiles } from '@main/utils/file'
|
||||
import { TraceMethod } from '@mcp-trace/trace-core'
|
||||
import { MB } from '@shared/config/constant'
|
||||
import type { LoaderReturn } from '@shared/config/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
@ -155,7 +156,7 @@ class KnowledgeService {
|
||||
await ragApplication.reset()
|
||||
}
|
||||
|
||||
public delete = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
|
||||
public async delete(_: Electron.IpcMainInvokeEvent, id: string): Promise<void> {
|
||||
logger.debug('delete id', id)
|
||||
const dbPath = path.join(this.storageDir, id)
|
||||
if (fs.existsSync(dbPath)) {
|
||||
@ -473,7 +474,7 @@ class KnowledgeService {
|
||||
})
|
||||
}
|
||||
|
||||
public add = async (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
|
||||
public add = (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
|
||||
return new Promise((resolve) => {
|
||||
const { base, item, forceReload = false, userId = '' } = options
|
||||
const optionsNonNullableAttribute = { base, item, forceReload, userId }
|
||||
@ -520,10 +521,11 @@ class KnowledgeService {
|
||||
})
|
||||
}
|
||||
|
||||
public remove = async (
|
||||
@TraceMethod({ spanName: 'remove', tag: 'Knowledge' })
|
||||
public async remove(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }
|
||||
): Promise<void> => {
|
||||
): Promise<void> {
|
||||
const ragApplication = await this.getRagApplication(base)
|
||||
logger.debug(`Remove Item UniqueId: ${uniqueId}`)
|
||||
for (const id of uniqueIds) {
|
||||
@ -531,18 +533,20 @@ class KnowledgeService {
|
||||
}
|
||||
}
|
||||
|
||||
public search = async (
|
||||
@TraceMethod({ spanName: 'RagSearch', tag: 'Knowledge' })
|
||||
public async search(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ search, base }: { search: string; base: KnowledgeBaseParams }
|
||||
): Promise<ExtractChunkData[]> => {
|
||||
): Promise<ExtractChunkData[]> {
|
||||
const ragApplication = await this.getRagApplication(base)
|
||||
return await ragApplication.search(search)
|
||||
}
|
||||
|
||||
public rerank = async (
|
||||
@TraceMethod({ spanName: 'rerank', tag: 'Knowledge' })
|
||||
public async rerank(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] }
|
||||
): Promise<ExtractChunkData[]> => {
|
||||
): Promise<ExtractChunkData[]> {
|
||||
if (results.length === 0) {
|
||||
return results
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { createInMemoryMCPServer } from '@main/mcpServers/factory'
|
||||
import { makeSureDirExists } from '@main/utils'
|
||||
import { buildFunctionCallToolName } from '@main/utils/mcp'
|
||||
import { getBinaryName, getBinaryPath } from '@main/utils/process'
|
||||
import { TraceMethod, withSpanFunc } from '@mcp-trace/trace-core'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
@ -26,7 +27,7 @@ import {
|
||||
ToolListChangedNotificationSchema
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import {
|
||||
import type {
|
||||
GetMCPPromptResponse,
|
||||
GetResourceResponse,
|
||||
MCPCallToolResponse,
|
||||
@ -49,6 +50,8 @@ import getLoginShellEnvironment from './mcp/shell-env'
|
||||
// Generic type for caching wrapped functions
|
||||
type CachedFunction<T extends unknown[], R> = (...args: T) => Promise<R>
|
||||
|
||||
type CallToolArgs = { server: MCPServer; name: string; args: any; callId?: string }
|
||||
|
||||
const logger = loggerService.withContext('MCPService')
|
||||
|
||||
/**
|
||||
@ -580,17 +583,22 @@ class McpService {
|
||||
}
|
||||
|
||||
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
const cachedListTools = withCache<[MCPServer], MCPTool[]>(
|
||||
this.listToolsImpl.bind(this),
|
||||
(server) => {
|
||||
const serverKey = this.getServerKey(server)
|
||||
return `mcp:list_tool:${serverKey}`
|
||||
},
|
||||
5 * 60 * 1000, // 5 minutes TTL
|
||||
`[MCP] Tools from ${server.name}`
|
||||
)
|
||||
const listFunc = (server: MCPServer) => {
|
||||
const cachedListTools = withCache<[MCPServer], MCPTool[]>(
|
||||
this.listToolsImpl.bind(this),
|
||||
(server) => {
|
||||
const serverKey = this.getServerKey(server)
|
||||
return `mcp:list_tool:${serverKey}`
|
||||
},
|
||||
5 * 60 * 1000, // 5 minutes TTL
|
||||
`[MCP] Tools from ${server.name}`
|
||||
)
|
||||
|
||||
return cachedListTools(server)
|
||||
const result = cachedListTools(server)
|
||||
return result
|
||||
}
|
||||
|
||||
return withSpanFunc(`${server.name}.ListTool`, 'MCP', listFunc, [server])
|
||||
}
|
||||
|
||||
/**
|
||||
@ -598,37 +606,41 @@ class McpService {
|
||||
*/
|
||||
public async callTool(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ server, name, args, callId }: { server: MCPServer; name: string; args: any; callId?: string }
|
||||
{ server, name, args, callId }: CallToolArgs
|
||||
): Promise<MCPCallToolResponse> {
|
||||
const toolCallId = callId || uuidv4()
|
||||
const abortController = new AbortController()
|
||||
this.activeToolCalls.set(toolCallId, abortController)
|
||||
|
||||
try {
|
||||
logger.debug('Calling:', server.name, name, args, 'callId:', toolCallId)
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = JSON.parse(args)
|
||||
} catch (e) {
|
||||
logger.error('args parse error', args)
|
||||
const callToolFunc = async ({ server, name, args }: CallToolArgs) => {
|
||||
try {
|
||||
logger.debug('Calling:', server.name, name, args, 'callId:', toolCallId)
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = JSON.parse(args)
|
||||
} catch (e) {
|
||||
logger.error('args parse error', args)
|
||||
}
|
||||
}
|
||||
const client = await this.initClient(server)
|
||||
const result = await client.callTool({ name, arguments: args }, undefined, {
|
||||
onprogress: (process) => {
|
||||
logger.debug(`Progress: ${process.progress / (process.total || 1)}`)
|
||||
window.api.mcp.setProgress(process.progress / (process.total || 1))
|
||||
},
|
||||
timeout: server.timeout ? server.timeout * 1000 : 60000, // Default timeout of 1 minute
|
||||
signal: this.activeToolCalls.get(toolCallId)?.signal
|
||||
})
|
||||
return result as MCPCallToolResponse
|
||||
} catch (error) {
|
||||
logger.error(`Error calling tool ${name} on ${server.name}:`, error)
|
||||
throw error
|
||||
} finally {
|
||||
this.activeToolCalls.delete(toolCallId)
|
||||
}
|
||||
const client = await this.initClient(server)
|
||||
const result = await client.callTool({ name, arguments: args }, undefined, {
|
||||
onprogress: (process) => {
|
||||
logger.debug(`Progress: ${process.progress / (process.total || 1)}`)
|
||||
window.api.mcp.setProgress(process.progress / (process.total || 1))
|
||||
},
|
||||
timeout: server.timeout ? server.timeout * 1000 : 60000, // Default timeout of 1 minute
|
||||
signal: this.activeToolCalls.get(toolCallId)?.signal
|
||||
})
|
||||
return result as MCPCallToolResponse
|
||||
} catch (error) {
|
||||
logger.error(`Error calling tool ${name} on ${server.name}:`, error)
|
||||
throw error
|
||||
} finally {
|
||||
this.activeToolCalls.delete(toolCallId)
|
||||
}
|
||||
|
||||
return await withSpanFunc(`${server.name}.${name}`, `MCP`, callToolFunc, [{ server, name, args }])
|
||||
}
|
||||
|
||||
public async getInstallInfo() {
|
||||
@ -695,6 +707,7 @@ class McpService {
|
||||
/**
|
||||
* Get a specific prompt from an MCP server with caching
|
||||
*/
|
||||
@TraceMethod({ spanName: 'getPrompt', tag: 'mcp' })
|
||||
public async getPrompt(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }
|
||||
@ -781,6 +794,7 @@ class McpService {
|
||||
/**
|
||||
* Get a specific resource from an MCP server with caching
|
||||
*/
|
||||
@TraceMethod({ spanName: 'getResource', tag: 'mcp' })
|
||||
public async getResource(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ server, uri }: { server: MCPServer; uri: string }
|
||||
|
||||
122
src/main/services/NodeTraceService.ts
Normal file
122
src/main/services/NodeTraceService.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { isDev } from '@main/constant'
|
||||
import { CacheBatchSpanProcessor, defaultConfig, FunctionSpanExporter } from '@mcp-trace/trace-core'
|
||||
import { NodeTracer as MCPNodeTracer } from '@mcp-trace/trace-node/nodeTracer'
|
||||
import { context, SpanContext, trace } from '@opentelemetry/api'
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import * as path from 'path'
|
||||
|
||||
import { ConfigKeys, configManager } from './ConfigManager'
|
||||
import { spanCacheService } from './SpanCacheService'
|
||||
|
||||
export const TRACER_NAME = 'CherryStudio'
|
||||
|
||||
export class NodeTraceService {
|
||||
init() {
|
||||
// TODO get developer mode setting from config
|
||||
defaultConfig.isDevModel = true
|
||||
|
||||
const exporter = new FunctionSpanExporter(async (spans) => {
|
||||
console.log(`Spans length:`, spans.length)
|
||||
})
|
||||
|
||||
MCPNodeTracer.init(
|
||||
{
|
||||
defaultTracerName: TRACER_NAME,
|
||||
serviceName: TRACER_NAME
|
||||
},
|
||||
new CacheBatchSpanProcessor(exporter, spanCacheService)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const originalHandle = ipcMain.handle
|
||||
ipcMain.handle = (channel: string, handler: (...args: any[]) => Promise<any>) => {
|
||||
return originalHandle.call(ipcMain, channel, async (event, ...args) => {
|
||||
const carray = args && args.length > 0 ? args[args.length - 1] : {}
|
||||
let ctx = context.active()
|
||||
let newArgs = args
|
||||
if (carray && typeof carray === 'object' && 'type' in carray && carray.type === 'trace') {
|
||||
const span = trace.wrapSpanContext(carray.context as SpanContext)
|
||||
ctx = trace.setSpan(context.active(), span)
|
||||
newArgs = args.slice(0, args.length - 1)
|
||||
}
|
||||
return context.with(ctx, () => handler(event, ...newArgs))
|
||||
})
|
||||
}
|
||||
|
||||
export const nodeTraceService = new NodeTraceService()
|
||||
|
||||
let traceWin: BrowserWindow | null = null
|
||||
|
||||
export function openTraceWindow(topicId: string, traceId: string, autoOpen = true, modelName?: string) {
|
||||
if (traceWin && !traceWin.isDestroyed()) {
|
||||
traceWin.focus()
|
||||
traceWin.webContents.send('set-trace', { traceId, topicId, modelName })
|
||||
return
|
||||
}
|
||||
|
||||
if (!traceWin && !autoOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
traceWin = new BrowserWindow({
|
||||
width: 600,
|
||||
minWidth: 500,
|
||||
minHeight: 600,
|
||||
height: 800,
|
||||
autoHideMenuBar: true,
|
||||
closable: true,
|
||||
focusable: true,
|
||||
movable: true,
|
||||
hasShadow: true,
|
||||
roundedCorners: true,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
resizable: true,
|
||||
title: 'Call Chain Window',
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: { height: 40 },
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
devTools: isDev ? true : false
|
||||
}
|
||||
})
|
||||
|
||||
if (isDev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
traceWin.loadURL(process.env['ELECTRON_RENDERER_URL'] + `/traceWindow.html`)
|
||||
} else {
|
||||
traceWin.loadFile(path.join(__dirname, '../renderer/traceWindow.html'))
|
||||
}
|
||||
traceWin.on('closed', () => {
|
||||
configManager.unsubscribe(ConfigKeys.Language, setLanguageCallback)
|
||||
try {
|
||||
traceWin?.destroy()
|
||||
} finally {
|
||||
traceWin = null
|
||||
}
|
||||
})
|
||||
|
||||
traceWin.webContents.on('did-finish-load', () => {
|
||||
traceWin!.webContents.send('set-trace', {
|
||||
traceId,
|
||||
topicId,
|
||||
modelName
|
||||
})
|
||||
traceWin!.webContents.send('set-language', { lang: configManager.get(ConfigKeys.Language) })
|
||||
configManager.subscribe(ConfigKeys.Language, setLanguageCallback)
|
||||
})
|
||||
}
|
||||
|
||||
const setLanguageCallback = (lang: string) => {
|
||||
traceWin!.webContents.send('set-language', { lang })
|
||||
}
|
||||
|
||||
export const setTraceWindowTitle = (title: string) => {
|
||||
if (traceWin) {
|
||||
traceWin.title = title
|
||||
}
|
||||
}
|
||||
409
src/main/services/SpanCacheService.ts
Normal file
409
src/main/services/SpanCacheService.ts
Normal file
@ -0,0 +1,409 @@
|
||||
import {
|
||||
Attributes,
|
||||
convertSpanToSpanEntity,
|
||||
defaultConfig,
|
||||
SpanEntity,
|
||||
TokenUsage,
|
||||
TraceCache
|
||||
} from '@mcp-trace/trace-core'
|
||||
import { SpanStatusCode } from '@opentelemetry/api'
|
||||
import { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||
import fs from 'fs/promises'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
|
||||
class SpanCacheService implements TraceCache {
|
||||
private topicMap: Map<string, string> = new Map<string, string>()
|
||||
private fileDir: string
|
||||
private cache: Map<string, SpanEntity> = new Map<string, SpanEntity>()
|
||||
pri
|
||||
|
||||
constructor() {
|
||||
this.fileDir = path.join(os.homedir(), '.cherrystudio', 'trace')
|
||||
}
|
||||
|
||||
createSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => {
|
||||
if (!defaultConfig.isDevModel) {
|
||||
return
|
||||
}
|
||||
const spanEntity = convertSpanToSpanEntity(span)
|
||||
spanEntity.topicId = this.topicMap.get(spanEntity.traceId)
|
||||
this.cache.set(span.spanContext().spanId, spanEntity)
|
||||
this._updateModelName(spanEntity)
|
||||
}
|
||||
|
||||
endSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => {
|
||||
if (!defaultConfig.isDevModel) {
|
||||
return
|
||||
}
|
||||
const spanId = span.spanContext().spanId
|
||||
const spanEntity = this.cache.get(spanId)
|
||||
if (!spanEntity) {
|
||||
return
|
||||
}
|
||||
|
||||
spanEntity.topicId = this.topicMap.get(spanEntity.traceId)
|
||||
spanEntity.endTime = span.endTime ? span.endTime[0] * 1e3 + Math.floor(span.endTime[1] / 1e6) : null
|
||||
spanEntity.status = SpanStatusCode[span.status.code]
|
||||
spanEntity.attributes = span.attributes ? ({ ...span.attributes } as Attributes) : {}
|
||||
spanEntity.events = span.events
|
||||
spanEntity.links = span.links
|
||||
this._updateModelName(spanEntity)
|
||||
}
|
||||
|
||||
clear: () => void = () => {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
async cleanTopic(topicId: string, traceId?: string, modelName?: string) {
|
||||
const spans = Array.from(this.cache.values().filter((e) => e.topicId === topicId))
|
||||
spans.map((e) => e.id).forEach((id) => this.cache.delete(id))
|
||||
|
||||
await this._checkFolder(path.join(this.fileDir, topicId))
|
||||
|
||||
if (modelName) {
|
||||
this.cleanHistoryTrace(topicId, traceId || '', modelName)
|
||||
this.saveSpans(topicId)
|
||||
} else if (traceId) {
|
||||
fs.rm(path.join(this.fileDir, topicId, traceId))
|
||||
} else {
|
||||
fs.readdir(path.join(this.fileDir, topicId)).then((files) =>
|
||||
files.forEach((file) => {
|
||||
fs.rm(path.join(this.fileDir, topicId, file))
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async cleanLocalData() {
|
||||
this.cache.clear()
|
||||
fs.readdir(this.fileDir)
|
||||
.then((files) =>
|
||||
files.forEach((topicId) => {
|
||||
fs.rm(path.join(this.fileDir, topicId), { recursive: true, force: true })
|
||||
})
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error('Error cleaning local data:', err)
|
||||
})
|
||||
}
|
||||
|
||||
async saveSpans(topicId: string) {
|
||||
if (!defaultConfig.isDevModel) {
|
||||
return
|
||||
}
|
||||
let traceId: string | undefined
|
||||
for (const [key, value] of this.topicMap.entries()) {
|
||||
if (value === topicId) {
|
||||
traceId = key
|
||||
break // 找到后立即退出循环
|
||||
}
|
||||
}
|
||||
if (!traceId) {
|
||||
return
|
||||
}
|
||||
const spans = Array.from(this.cache.values().filter((e) => e.traceId === traceId || !e.modelName))
|
||||
await this._saveToFile(spans, traceId, topicId)
|
||||
this.topicMap.delete(traceId)
|
||||
this._cleanCache(traceId)
|
||||
}
|
||||
|
||||
async getSpans(topicId: string, traceId: string, modelName?: string) {
|
||||
if (this.topicMap.has(traceId)) {
|
||||
const spans: SpanEntity[] = []
|
||||
this.cache
|
||||
.values()
|
||||
.filter((spanEntity) => {
|
||||
return spanEntity.traceId === traceId && spanEntity.modelName
|
||||
})
|
||||
.filter((spanEntity) => {
|
||||
return !modelName || spanEntity.modelName === modelName
|
||||
})
|
||||
.forEach((sp) => spans.push(sp))
|
||||
return spans
|
||||
} else {
|
||||
return this._getHisData(topicId, traceId, modelName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* binding topic id to trace
|
||||
* @param traceId traceId
|
||||
* @param topicId topicId
|
||||
*/
|
||||
setTopicId(traceId: string, topicId: string): void {
|
||||
this.topicMap.set(traceId, topicId)
|
||||
}
|
||||
|
||||
getEntity(spanId: string): SpanEntity | undefined {
|
||||
return this.cache.get(spanId)
|
||||
}
|
||||
|
||||
saveEntity(entity: SpanEntity) {
|
||||
if (!defaultConfig.isDevModel) {
|
||||
return
|
||||
}
|
||||
if (this.cache.has(entity.id)) {
|
||||
this._updateEntity(entity)
|
||||
} else {
|
||||
this._addEntity(entity)
|
||||
}
|
||||
this._updateModelName(entity)
|
||||
}
|
||||
|
||||
updateTokenUsage(spanId: string, usage: TokenUsage) {
|
||||
const entity = this.cache.get(spanId)
|
||||
if (entity) {
|
||||
entity.usage = { ...usage }
|
||||
}
|
||||
if (entity?.parentId) {
|
||||
this._updateParentUsage(entity.parentId, usage)
|
||||
}
|
||||
}
|
||||
|
||||
addStreamMessage(spanId: string, modelName: string, context: string, message: any) {
|
||||
const span = this.cache.get(spanId)
|
||||
if (!span) {
|
||||
return
|
||||
}
|
||||
const attributes = span.attributes
|
||||
let msgArray: any[] = []
|
||||
if (attributes && attributes['outputs'] && Array.isArray(attributes['outputs'])) {
|
||||
msgArray = attributes['outputs'] || []
|
||||
msgArray.push(message)
|
||||
attributes['outputs'] = msgArray
|
||||
} else {
|
||||
msgArray = [message]
|
||||
span.attributes = { ...attributes, outputs: msgArray } as Attributes
|
||||
}
|
||||
this._updateParentOutputs(span.parentId, modelName, context)
|
||||
}
|
||||
|
||||
setEndMessage(spanId: string, modelName: string, message: string) {
|
||||
const span = this.cache.get(spanId)
|
||||
if (span && span.attributes) {
|
||||
let outputs = span.attributes['outputs']
|
||||
if (!outputs || typeof outputs !== 'object') {
|
||||
outputs = {}
|
||||
}
|
||||
if (!(`${modelName}` in outputs) || !outputs[`${modelName}`]) {
|
||||
outputs[`${modelName}`] = message
|
||||
span.attributes[`outputs`] = outputs
|
||||
this.cache.set(spanId, span)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cleanHistoryTrace(topicId: string, traceId: string, modelName?: string) {
|
||||
this._cleanCache(traceId, modelName)
|
||||
|
||||
const filePath = path.join(this.fileDir, topicId, traceId)
|
||||
const fileExists = await this._existFile(filePath)
|
||||
|
||||
if (!fileExists) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!modelName) {
|
||||
await fs.rm(filePath, { recursive: true })
|
||||
} else {
|
||||
const allSpans = await this._getHisData(topicId, traceId)
|
||||
allSpans.forEach((span) => {
|
||||
if (!modelName || modelName !== span.modelName) {
|
||||
this.cache.set(span.id, span)
|
||||
}
|
||||
})
|
||||
try {
|
||||
await fs.rm(filePath, { recursive: true })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _addEntity(entity: SpanEntity): void {
|
||||
entity.topicId = this.topicMap.get(entity.traceId)
|
||||
this.cache.set(entity.id, entity)
|
||||
}
|
||||
|
||||
private _updateModelName(entity: SpanEntity) {
|
||||
let modelName = entity.modelName || entity.attributes?.modelName?.toString()
|
||||
if (!modelName && entity.parentId) {
|
||||
modelName = this.cache.get(entity.parentId)?.modelName
|
||||
}
|
||||
entity.modelName = modelName
|
||||
}
|
||||
private _updateEntity(entity: SpanEntity): void {
|
||||
entity.topicId = this.topicMap.get(entity.traceId)
|
||||
const savedEntity = this.cache.get(entity.id)
|
||||
if (savedEntity) {
|
||||
Object.keys(entity).forEach((key) => {
|
||||
const value = entity[key]
|
||||
if (value === undefined) {
|
||||
savedEntity[key] = value
|
||||
return
|
||||
}
|
||||
if (key === 'attributes') {
|
||||
const savedAttrs = savedEntity.attributes || {}
|
||||
Object.keys(value).forEach((attrKey) => {
|
||||
const jsonData =
|
||||
typeof value[attrKey] === 'string' && value[attrKey].startsWith('{')
|
||||
? JSON.parse(value[attrKey])
|
||||
: value[attrKey]
|
||||
if (
|
||||
savedAttrs[attrKey] !== undefined &&
|
||||
typeof jsonData === 'object' &&
|
||||
typeof savedAttrs[attrKey] === 'object'
|
||||
) {
|
||||
savedAttrs[attrKey] = { ...savedAttrs[attrKey], ...jsonData }
|
||||
} else {
|
||||
savedAttrs[attrKey] = value[attrKey]
|
||||
}
|
||||
})
|
||||
savedEntity.attributes = savedAttrs
|
||||
} else {
|
||||
savedEntity[key] = value
|
||||
}
|
||||
})
|
||||
this.cache.set(entity.id, savedEntity)
|
||||
}
|
||||
}
|
||||
|
||||
private _cleanCache(traceId: string, modelName?: string) {
|
||||
this.cache
|
||||
.values()
|
||||
.filter((span) => {
|
||||
return span && span.traceId === traceId && (!modelName || span.modelName === modelName)
|
||||
})
|
||||
.forEach((span) => this.cache.delete(span.id))
|
||||
}
|
||||
|
||||
private _updateParentOutputs(spanId: string, modelName: string, context: string) {
|
||||
const span = this.cache.get(spanId)
|
||||
if (!span || !context) {
|
||||
return
|
||||
}
|
||||
const attributes = span.attributes
|
||||
// 如果含有modelName属性,是具体的某个modalName输出,拼接到streamText下面
|
||||
if (attributes && span.modelName) {
|
||||
const currentValue = attributes['outputs']
|
||||
if (currentValue && typeof currentValue === 'object') {
|
||||
const allContext = (currentValue['streamText'] || '') + context
|
||||
attributes['outputs'] = { ...currentValue, streamText: allContext }
|
||||
} else {
|
||||
attributes['outputs'] = { streamText: context }
|
||||
}
|
||||
span.attributes = attributes
|
||||
} else if (span.modelName) {
|
||||
span.attributes = { outputs: { [`${modelName}`]: context } } as Attributes
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.cache.set(span.id, span)
|
||||
this._updateParentOutputs(span.parentId, modelName, context)
|
||||
}
|
||||
|
||||
private _updateParentUsage(spanId: string, usage: TokenUsage) {
|
||||
const entity = this.cache.get(spanId)
|
||||
if (!entity) {
|
||||
return
|
||||
}
|
||||
if (!entity.usage) {
|
||||
entity.usage = { ...usage }
|
||||
} else {
|
||||
entity.usage.prompt_tokens = entity.usage.prompt_tokens + usage.prompt_tokens
|
||||
entity.usage.completion_tokens = entity.usage.completion_tokens + usage.completion_tokens
|
||||
entity.usage.total_tokens = entity.usage.total_tokens + usage.total_tokens
|
||||
}
|
||||
this.cache.set(entity.id, entity)
|
||||
if (entity?.parentId) {
|
||||
this._updateParentUsage(entity.parentId, usage)
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveToFile(spans: SpanEntity[], traceId: string, topicId: string) {
|
||||
const dirPath = path.join(this.fileDir, topicId)
|
||||
await this._checkFolder(dirPath)
|
||||
|
||||
const filePath = path.join(dirPath, traceId)
|
||||
|
||||
const writeOperations = spans
|
||||
.filter((span) => span.topicId)
|
||||
.map(async (span) => {
|
||||
await fs.appendFile(filePath, JSON.stringify(span) + '\n')
|
||||
})
|
||||
|
||||
await Promise.all(writeOperations)
|
||||
}
|
||||
|
||||
private async _getHisData(topicId: string, traceId: string, modelName?: string) {
|
||||
const filePath = path.join(this.fileDir, topicId, traceId)
|
||||
|
||||
if (!(await this._existFile(filePath))) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const fileHandle = await fs.open(filePath, 'r')
|
||||
const stream = fileHandle.createReadStream()
|
||||
const chunks: string[] = []
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk.toString())
|
||||
}
|
||||
await fileHandle.close()
|
||||
|
||||
// 使用生成器逐行处理
|
||||
const parseLines = function* (text: string) {
|
||||
for (const line of text.split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed) {
|
||||
try {
|
||||
yield JSON.parse(trimmed) as SpanEntity
|
||||
} catch (e) {
|
||||
console.error(`JSON解析失败: ${trimmed}`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(parseLines(chunks.join('')))
|
||||
.filter((span) => span.topicId === topicId && span.traceId === traceId && span.modelName)
|
||||
.filter((span) => !modelName || span.modelName === modelName)
|
||||
} catch (err) {
|
||||
console.error('Error parsing JSON:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkFolder(filePath: string) {
|
||||
try {
|
||||
await fs.mkdir(filePath, { recursive: true })
|
||||
} catch (err) {
|
||||
if (typeof err === 'object' && err && 'code' in err && err.code !== 'EEXIST') throw err
|
||||
}
|
||||
}
|
||||
|
||||
private async _existFile(filePath: string) {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.log('delete trace file error:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const spanCacheService = new SpanCacheService()
|
||||
export const cleanTopic = spanCacheService.cleanTopic.bind(spanCacheService)
|
||||
export const saveEntity = spanCacheService.saveEntity.bind(spanCacheService)
|
||||
export const getEntity = spanCacheService.getEntity.bind(spanCacheService)
|
||||
export const tokenUsage = spanCacheService.updateTokenUsage.bind(spanCacheService)
|
||||
export const saveSpans = spanCacheService.saveSpans.bind(spanCacheService)
|
||||
export const getSpans = spanCacheService.getSpans.bind(spanCacheService)
|
||||
export const addEndMessage = spanCacheService.setEndMessage.bind(spanCacheService)
|
||||
export const bindTopic = spanCacheService.setTopicId.bind(spanCacheService)
|
||||
export const addStreamMessage = spanCacheService.addStreamMessage.bind(spanCacheService)
|
||||
export const cleanHistoryTrace = spanCacheService.cleanHistoryTrace.bind(spanCacheService)
|
||||
export const cleanLocalData = spanCacheService.cleanLocalData.bind(spanCacheService)
|
||||
@ -1,5 +1,7 @@
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { SpanContext } from '@opentelemetry/api'
|
||||
import { UpgradeChannel } from '@shared/config/constant'
|
||||
import type { LogLevel, LogSourceWithContext } from '@shared/config/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
@ -26,6 +28,14 @@ import { Notification } from 'src/renderer/src/types/notification'
|
||||
import { CreateDirectoryOptions } from 'webdav'
|
||||
|
||||
import type { ActionItem } from '../renderer/src/types/selectionTypes'
|
||||
export function tracedInvoke(channel: string, spanContext: SpanContext | undefined, ...args: any[]) {
|
||||
if (spanContext) {
|
||||
const data = { type: 'trace', context: spanContext }
|
||||
console.log(`tracedInvoke data`, data)
|
||||
return ipcRenderer.invoke(channel, ...args, data)
|
||||
}
|
||||
return ipcRenderer.invoke(channel, ...args)
|
||||
}
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
@ -125,7 +135,7 @@ const api = {
|
||||
deleteDir: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_DeleteDir, dirPath),
|
||||
read: (fileId: string, detectEncoding?: boolean) =>
|
||||
ipcRenderer.invoke(IpcChannel.File_Read, fileId, detectEncoding),
|
||||
clear: () => ipcRenderer.invoke(IpcChannel.File_Clear),
|
||||
clear: (spanContext?: SpanContext) => ipcRenderer.invoke(IpcChannel.File_Clear, spanContext),
|
||||
get: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Get, filePath),
|
||||
/**
|
||||
* 创建一个空的临时文件
|
||||
@ -145,7 +155,7 @@ const api = {
|
||||
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
|
||||
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) =>
|
||||
ipcRenderer.invoke(IpcChannel.File_Save, path, content, options),
|
||||
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
|
||||
selectFolder: (spanContext?: SpanContext) => ipcRenderer.invoke(IpcChannel.File_SelectFolder, spanContext),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
|
||||
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
|
||||
base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
|
||||
@ -169,7 +179,8 @@ const api = {
|
||||
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke(IpcChannel.Shortcuts_Update, shortcuts)
|
||||
},
|
||||
knowledgeBase: {
|
||||
create: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Create, base),
|
||||
create: (base: KnowledgeBaseParams, context?: SpanContext) =>
|
||||
tracedInvoke(IpcChannel.KnowledgeBase_Create, context, base),
|
||||
reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Reset, base),
|
||||
delete: (id: string) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Delete, id),
|
||||
add: ({
|
||||
@ -185,10 +196,12 @@ const api = {
|
||||
}) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Add, { base, item, forceReload, userId }),
|
||||
remove: ({ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }) =>
|
||||
ipcRenderer.invoke(IpcChannel.KnowledgeBase_Remove, { uniqueId, uniqueIds, base }),
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
|
||||
ipcRenderer.invoke(IpcChannel.KnowledgeBase_Search, { search, base }),
|
||||
rerank: ({ search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] }) =>
|
||||
ipcRenderer.invoke(IpcChannel.KnowledgeBase_Rerank, { search, base, results }),
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }, context?: SpanContext) =>
|
||||
tracedInvoke(IpcChannel.KnowledgeBase_Search, context, { search, base }),
|
||||
rerank: (
|
||||
{ search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] },
|
||||
context?: SpanContext
|
||||
) => tracedInvoke(IpcChannel.KnowledgeBase_Rerank, context, { search, base, results }),
|
||||
checkQuota: ({ base, userId }: { base: KnowledgeBaseParams; userId: string }) =>
|
||||
ipcRenderer.invoke(IpcChannel.KnowledgeBase_Check_Quota, base, userId)
|
||||
},
|
||||
@ -253,9 +266,11 @@ const api = {
|
||||
removeServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RemoveServer, server),
|
||||
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
|
||||
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
|
||||
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
|
||||
callTool: ({ server, name, args, callId }: { server: MCPServer; name: string; args: any; callId?: string }) =>
|
||||
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args, callId }),
|
||||
listTools: (server: MCPServer, context?: SpanContext) => tracedInvoke(IpcChannel.Mcp_ListTools, context, server),
|
||||
callTool: (
|
||||
{ server, name, args, callId }: { server: MCPServer; name: string; args: any; callId?: string },
|
||||
context?: SpanContext
|
||||
) => tracedInvoke(IpcChannel.Mcp_CallTool, context, { server, name, args, callId }),
|
||||
listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server),
|
||||
getPrompt: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
|
||||
ipcRenderer.invoke(IpcChannel.Mcp_GetPrompt, { server, name, args }),
|
||||
@ -348,7 +363,28 @@ const api = {
|
||||
},
|
||||
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
|
||||
setDisableHardwareAcceleration: (isDisable: boolean) =>
|
||||
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable)
|
||||
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
|
||||
trace: {
|
||||
saveData: (topicId: string) => ipcRenderer.invoke(IpcChannel.TRACE_SAVE_DATA, topicId),
|
||||
getData: (topicId: string, traceId: string, modelName?: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.TRACE_GET_DATA, topicId, traceId, modelName),
|
||||
saveEntity: (entity: SpanEntity) => ipcRenderer.invoke(IpcChannel.TRACE_SAVE_ENTITY, entity),
|
||||
getEntity: (spanId: string) => ipcRenderer.invoke(IpcChannel.TRACE_GET_ENTITY, spanId),
|
||||
bindTopic: (topicId: string, traceId: string) => ipcRenderer.invoke(IpcChannel.TRACE_BIND_TOPIC, topicId, traceId),
|
||||
tokenUsage: (spanId: string, usage: TokenUsage) => ipcRenderer.invoke(IpcChannel.TRACE_TOKEN_USAGE, spanId, usage),
|
||||
cleanHistory: (topicId: string, traceId: string, modelName?: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.TRACE_CLEAN_HISTORY, topicId, traceId, modelName),
|
||||
cleanTopic: (topicId: string, traceId?: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.TRACE_CLEAN_TOPIC, topicId, traceId),
|
||||
openWindow: (topicId: string, traceId: string, autoOpen?: boolean, modelName?: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.TRACE_OPEN_WINDOW, topicId, traceId, autoOpen, modelName),
|
||||
setTraceWindowTitle: (title: string) => ipcRenderer.invoke(IpcChannel.TRACE_SET_TITLE, title),
|
||||
addEndMessage: (spanId: string, modelName: string, context: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.TRACE_ADD_END_MESSAGE, spanId, modelName, context),
|
||||
cleanLocalData: () => ipcRenderer.invoke(IpcChannel.TRACE_CLEAN_LOCAL_DATA),
|
||||
addStreamMessage: (spanId: string, modelName: string, context: string, message: any) =>
|
||||
ipcRenderer.invoke(IpcChannel.TRACE_ADD_STREAM_MESSAGE, spanId, modelName, context, message)
|
||||
}
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@ -543,7 +543,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
|
||||
const sdkParams: OpenAISdkParams = streamOutput
|
||||
? {
|
||||
...commonParams,
|
||||
stream: true
|
||||
stream: true,
|
||||
stream_options: { include_usage: true }
|
||||
}
|
||||
: {
|
||||
...commonParams,
|
||||
|
||||
@ -2,9 +2,11 @@ import { loggerService } from '@logger'
|
||||
import { ApiClientFactory } from '@renderer/aiCore/clients/ApiClientFactory'
|
||||
import { BaseApiClient } from '@renderer/aiCore/clients/BaseApiClient'
|
||||
import { isDedicatedImageGenerationModel, isFunctionCallingModel } from '@renderer/config/models'
|
||||
import { withSpanResult } from '@renderer/services/SpanManagerService'
|
||||
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { GenerateImageParams, Model, Provider } from '@renderer/types'
|
||||
import { RequestOptions, SdkModel } from '@renderer/types/sdk'
|
||||
import type { RequestOptions, SdkModel } from '@renderer/types/sdk'
|
||||
import { isEnabledToolUse } from '@renderer/utils/mcp-tools'
|
||||
|
||||
import { OpenAIAPIClient } from './clients'
|
||||
@ -25,7 +27,7 @@ import { MIDDLEWARE_NAME as ImageGenerationMiddlewareName } from './middleware/f
|
||||
import { MIDDLEWARE_NAME as ThinkingTagExtractionMiddlewareName } from './middleware/feat/ThinkingTagExtractionMiddleware'
|
||||
import { MIDDLEWARE_NAME as ToolUseExtractionMiddlewareName } from './middleware/feat/ToolUseExtractionMiddleware'
|
||||
import { MiddlewareRegistry } from './middleware/register'
|
||||
import { CompletionsParams, CompletionsResult } from './middleware/schemas'
|
||||
import type { CompletionsParams, CompletionsResult } from './middleware/schemas'
|
||||
|
||||
const logger = loggerService.withContext('AiProvider')
|
||||
|
||||
@ -126,7 +128,23 @@ export default class AiProvider {
|
||||
const wrappedCompletionMethod = applyCompletionsMiddlewares(client, client.createCompletions, middlewares)
|
||||
|
||||
// 4. Execute the wrapped method with the original params
|
||||
return wrappedCompletionMethod(params, options)
|
||||
const result = wrappedCompletionMethod(params, options)
|
||||
return result
|
||||
}
|
||||
|
||||
public async completionsForTrace(params: CompletionsParams, options?: RequestOptions): Promise<CompletionsResult> {
|
||||
const traceName = params.assistant.model?.name
|
||||
? `${params.assistant.model?.name}.${params.callType}`
|
||||
: `LLM.${params.callType}`
|
||||
|
||||
const traceParams: StartSpanParams = {
|
||||
name: traceName,
|
||||
tag: 'LLM',
|
||||
topicId: params.topicId || '',
|
||||
modelName: params.assistant.model?.name
|
||||
}
|
||||
|
||||
return await withSpanResult(this.completions.bind(this), traceParams, params, options)
|
||||
}
|
||||
|
||||
public async models(): Promise<SdkModel[]> {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { withSpanResult } from '@renderer/services/SpanManagerService'
|
||||
import {
|
||||
RequestOptions,
|
||||
SdkInstance,
|
||||
@ -252,19 +253,28 @@ export function applyCompletionsMiddlewares<
|
||||
const abortSignal = context._internal.flowControl?.abortSignal
|
||||
const timeout = context._internal.customState?.sdkMetadata?.timeout
|
||||
|
||||
const methodCall = async (payload) => {
|
||||
return await originalCompletionsMethod.call(originalApiClientInstance, payload, {
|
||||
...options,
|
||||
signal: abortSignal,
|
||||
timeout
|
||||
})
|
||||
}
|
||||
|
||||
const traceParams = {
|
||||
name: `${params.assistant?.model?.name}.client`,
|
||||
tag: 'LLM',
|
||||
topicId: params.topicId || '',
|
||||
modelName: params.assistant?.model?.name
|
||||
}
|
||||
|
||||
// Call the original SDK method with transformed parameters
|
||||
// 使用转换后的参数调用原始 SDK 方法
|
||||
const rawOutput = await originalCompletionsMethod.call(originalApiClientInstance, sdkPayload, {
|
||||
...options,
|
||||
signal: abortSignal,
|
||||
timeout
|
||||
})
|
||||
const rawOutput = await withSpanResult(methodCall, traceParams, sdkPayload)
|
||||
|
||||
// Return result wrapped in CompletionsResult format
|
||||
// 以 CompletionsResult 格式返回包装的结果
|
||||
return {
|
||||
rawOutput
|
||||
} as CompletionsResult
|
||||
return { rawOutput } as CompletionsResult
|
||||
}
|
||||
|
||||
const chain = middlewares.map((middleware) => middleware(api))
|
||||
|
||||
@ -119,7 +119,8 @@ function createToolHandlingTransform(
|
||||
mcpTools,
|
||||
allToolResponses,
|
||||
currentParams.onChunk,
|
||||
currentParams.assistant.model!
|
||||
currentParams.assistant.model!,
|
||||
currentParams.topicId
|
||||
)
|
||||
|
||||
// 缓存执行结果
|
||||
@ -147,7 +148,8 @@ function createToolHandlingTransform(
|
||||
mcpTools,
|
||||
allToolResponses,
|
||||
currentParams.onChunk,
|
||||
currentParams.assistant.model!
|
||||
currentParams.assistant.model!,
|
||||
currentParams.topicId
|
||||
)
|
||||
|
||||
// 缓存执行结果
|
||||
@ -217,7 +219,8 @@ async function executeToolCalls(
|
||||
mcpTools: MCPTool[],
|
||||
allToolResponses: MCPToolResponse[],
|
||||
onChunk: CompletionsParams['onChunk'],
|
||||
model: Model
|
||||
model: Model,
|
||||
topicId?: string
|
||||
): Promise<{ toolResults: SdkMessageParam[]; confirmedToolCalls: SdkToolCall[] }> {
|
||||
const mcpToolResponses: ToolCallResponse[] = toolCalls
|
||||
.map((toolCall) => {
|
||||
@ -244,7 +247,8 @@ async function executeToolCalls(
|
||||
},
|
||||
model,
|
||||
mcpTools,
|
||||
ctx._internal?.flowControl?.abortSignal
|
||||
ctx._internal?.flowControl?.abortSignal,
|
||||
topicId
|
||||
)
|
||||
|
||||
// 找出已确认工具对应的原始toolCalls
|
||||
@ -275,7 +279,8 @@ async function executeToolUseResponses(
|
||||
mcpTools: MCPTool[],
|
||||
allToolResponses: MCPToolResponse[],
|
||||
onChunk: CompletionsParams['onChunk'],
|
||||
model: Model
|
||||
model: Model,
|
||||
topicId?: CompletionsParams['topicId']
|
||||
): Promise<{ toolResults: SdkMessageParam[] }> {
|
||||
// 直接使用parseAndCallTools函数处理已经解析好的ToolUseResponse
|
||||
const { toolResults } = await parseAndCallTools(
|
||||
@ -287,7 +292,8 @@ async function executeToolUseResponses(
|
||||
},
|
||||
model,
|
||||
mcpTools,
|
||||
ctx._internal?.flowControl?.abortSignal
|
||||
ctx._internal?.flowControl?.abortSignal,
|
||||
topicId
|
||||
)
|
||||
|
||||
return { toolResults }
|
||||
|
||||
@ -55,6 +55,7 @@ export interface CompletionsParams {
|
||||
|
||||
// 上下文控制
|
||||
contextCount?: number
|
||||
topicId?: string // 主题ID,用于关联上下文
|
||||
|
||||
_internal?: ProcessingState
|
||||
}
|
||||
|
||||
@ -73,7 +73,6 @@ db.version(7)
|
||||
message_blocks: 'id, messageId, file.id' // Correct syntax with comma separator
|
||||
})
|
||||
.upgrade((tx) => upgradeToV7(tx))
|
||||
|
||||
db.version(8)
|
||||
.stores({
|
||||
// Re-declare all tables for the new version
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { appendTrace, pauseTrace, restartTrace } from '@renderer/services/SpanManagerService'
|
||||
import { estimateUserPromptUsage } from '@renderer/services/TokenService'
|
||||
import store, { type RootState, useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { updateOneBlock } from '@renderer/store/messageBlock'
|
||||
@ -53,8 +54,9 @@ export function useMessageOperations(topic: Topic) {
|
||||
* Dispatches deleteSingleMessageThunk.
|
||||
*/
|
||||
const deleteMessage = useCallback(
|
||||
async (id: string) => {
|
||||
async (id: string, traceId?: string, modelName?: string) => {
|
||||
await dispatch(deleteSingleMessageThunk(topic.id, id))
|
||||
window.api.trace.cleanHistory(topic.id, traceId || '', modelName)
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
)
|
||||
@ -99,6 +101,7 @@ export function useMessageOperations(topic: Topic) {
|
||||
*/
|
||||
const resendMessage = useCallback(
|
||||
async (message: Message, assistant: Assistant) => {
|
||||
await restartTrace(message)
|
||||
await dispatch(resendMessageThunk(topic.id, message, assistant))
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
@ -139,6 +142,7 @@ export function useMessageOperations(topic: Topic) {
|
||||
for (const askId of askIds) {
|
||||
abortCompletion(askId)
|
||||
}
|
||||
pauseTrace(topic.id)
|
||||
dispatch(newMessagesActions.setTopicLoading({ topicId: topic.id, loading: false }))
|
||||
}, [topic.id, dispatch])
|
||||
|
||||
@ -158,6 +162,7 @@ export function useMessageOperations(topic: Topic) {
|
||||
*/
|
||||
const regenerateAssistantMessage = useCallback(
|
||||
async (message: Message, assistant: Assistant) => {
|
||||
await restartTrace(message)
|
||||
if (message.role !== 'assistant') {
|
||||
logger.warn('regenerateAssistantMessage should only be called for assistant messages.')
|
||||
return
|
||||
@ -173,6 +178,7 @@ export function useMessageOperations(topic: Topic) {
|
||||
*/
|
||||
const appendAssistantResponse = useCallback(
|
||||
async (existingAssistantMessage: Message, newModel: Model, assistant: Assistant) => {
|
||||
await appendTrace(existingAssistantMessage, newModel)
|
||||
if (existingAssistantMessage.role !== 'assistant') {
|
||||
logger.error('appendAssistantResponse should only be called for an existing assistant message.')
|
||||
return
|
||||
@ -181,7 +187,15 @@ export function useMessageOperations(topic: Topic) {
|
||||
logger.error('Cannot append response: The existing assistant message is missing its askId.')
|
||||
return
|
||||
}
|
||||
await dispatch(appendAssistantResponseThunk(topic.id, existingAssistantMessage.id, newModel, assistant))
|
||||
await dispatch(
|
||||
appendAssistantResponseThunk(
|
||||
topic.id,
|
||||
existingAssistantMessage.id,
|
||||
newModel,
|
||||
assistant,
|
||||
existingAssistantMessage.traceId
|
||||
)
|
||||
)
|
||||
},
|
||||
[dispatch, topic.id]
|
||||
)
|
||||
@ -375,6 +389,8 @@ export function useMessageOperations(topic: Topic) {
|
||||
return
|
||||
}
|
||||
|
||||
await restartTrace(message, mainTextBlock.content)
|
||||
|
||||
const fileBlocks = editedBlocks.filter(
|
||||
(block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE
|
||||
)
|
||||
|
||||
@ -2561,9 +2561,132 @@
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "Knowledge Graph",
|
||||
"quit": "[to be translated]:退出",
|
||||
"quit": "Quit",
|
||||
"show_window": "Show Window",
|
||||
"visualization": "Visualization"
|
||||
},
|
||||
"memory": {
|
||||
"title": "Memories",
|
||||
"actions": "Actions",
|
||||
"description": "Memory allows you to store and manage information about your interactions with the assistant. You can add, edit, and delete memories, as well as filter and search through them.",
|
||||
"add_memory": "Add Memory",
|
||||
"edit_memory": "Edit Memory",
|
||||
"memory_content": "Memory Content",
|
||||
"please_enter_memory": "Please enter memory content",
|
||||
"memory_placeholder": "Enter memory content...",
|
||||
"user_id": "User ID",
|
||||
"user_id_placeholder": "Enter user ID (optional)",
|
||||
"load_failed": "Failed to load memories",
|
||||
"add_success": "Memory added successfully",
|
||||
"add_failed": "Failed to add memory",
|
||||
"update_success": "Memory updated successfully",
|
||||
"update_failed": "Failed to update memory",
|
||||
"delete_success": "Memory deleted successfully",
|
||||
"delete_failed": "Failed to delete memory",
|
||||
"delete_confirm_title": "Delete Memories",
|
||||
"delete_confirm_content": "Are you sure you want to delete {{count}} memories?",
|
||||
"delete_confirm": "Are you sure you want to delete this memory?",
|
||||
"time": "Time",
|
||||
"user": "User",
|
||||
"content": "Content",
|
||||
"score": "Score",
|
||||
"memories_description": "Showing {{count}} of {{total}} memories",
|
||||
"search_placeholder": "Search memories...",
|
||||
"start_date": "Start Date",
|
||||
"end_date": "End Date",
|
||||
"all_users": "All Users",
|
||||
"users": "users",
|
||||
"delete_selected": "Delete Selected",
|
||||
"reset_filters": "Reset Filters",
|
||||
"pagination_total": "{{start}}-{{end}} of {{total}} items",
|
||||
"current_user": "Current User",
|
||||
"select_user": "Select User",
|
||||
"default_user": "Default User",
|
||||
"switch_user": "Switch User",
|
||||
"user_switched": "User context switched to {{user}}",
|
||||
"switch_user_confirm": "Switch user context to {{user}}?",
|
||||
"add_user": "Add User",
|
||||
"add_new_user": "Add New User",
|
||||
"new_user_id": "New User ID",
|
||||
"new_user_id_placeholder": "Enter a unique user ID",
|
||||
"user_id_required": "User ID is required",
|
||||
"user_id_reserved": "'default-user' is reserved, please use a different ID",
|
||||
"user_id_exists": "This user ID already exists",
|
||||
"user_id_too_long": "User ID cannot exceed 50 characters",
|
||||
"user_id_invalid_chars": "User ID can only contain letters, numbers, hyphens and underscores",
|
||||
"user_id_rules": "User ID must be unique and contain only letters, numbers, hyphens (-) and underscores (_)",
|
||||
"user_created": "User {{user}} created and switched successfully",
|
||||
"add_user_failed": "Failed to add user",
|
||||
"memory": "memory",
|
||||
"reset_user_memories": "Reset User Memories",
|
||||
"reset_memories": "Reset Memories",
|
||||
"delete_user": "Delete User",
|
||||
"loading_memories": "Loading memories...",
|
||||
"no_memories": "No memories yet",
|
||||
"no_matching_memories": "No matching memories found",
|
||||
"no_memories_description": "Start by adding your first memory to get started",
|
||||
"try_different_filters": "Try adjusting your search criteria",
|
||||
"add_first_memory": "Add Your First Memory",
|
||||
"user_switch_failed": "Failed to switch user",
|
||||
"cannot_delete_default_user": "Cannot delete the default user",
|
||||
"delete_user_confirm_title": "Delete User",
|
||||
"delete_user_confirm_content": "Are you sure you want to delete user {{user}} and all their memories?",
|
||||
"user_deleted": "User {{user}} deleted successfully",
|
||||
"delete_user_failed": "Failed to delete user",
|
||||
"reset_user_memories_confirm_title": "Reset User Memories",
|
||||
"reset_user_memories_confirm_content": "Are you sure you want to reset all memories for {{user}}?",
|
||||
"user_memories_reset": "All memories for {{user}} have been reset",
|
||||
"reset_user_memories_failed": "Failed to reset user memories",
|
||||
"reset_memories_confirm_title": "Reset All Memories",
|
||||
"reset_memories_confirm_content": "Are you sure you want to permanently delete all memories for {{user}}? This action cannot be undone.",
|
||||
"memories_reset_success": "All memories for {{user}} have been reset successfully",
|
||||
"reset_memories_failed": "Failed to reset memories",
|
||||
"delete_confirm_single": "Are you sure you want to delete this memory?",
|
||||
"total_memories": "total memories",
|
||||
"default": "Default",
|
||||
"custom": "Custom",
|
||||
"global_memory_enabled": "Global memory enabled",
|
||||
"global_memory": "Global Memory",
|
||||
"enable_global_memory_first": "Please enable global memory first",
|
||||
"configure_memory_first": "Please configure memory settings first",
|
||||
"global_memory_disabled_title": "Global Memory Disabled",
|
||||
"global_memory_disabled_desc": "To use memory features, please enable global memory in assistant settings first.",
|
||||
"not_configured_title": "Memory Not Configured",
|
||||
"not_configured_desc": "Please configure embedding and LLM models in memory settings to enable memory functionality.",
|
||||
"go_to_memory_page": "Go to Memory Page",
|
||||
"settings": "Settings",
|
||||
"user_management": "User Management",
|
||||
"statistics": "Statistics",
|
||||
"search": "Search",
|
||||
"initial_memory_content": "Welcome! This is your first memory.",
|
||||
"loading": "Loading memories...",
|
||||
"settings_title": "Memory Settings",
|
||||
"llm_model": "LLM Model",
|
||||
"please_select_llm_model": "Please select an LLM model",
|
||||
"select_llm_model_placeholder": "Select LLM Model",
|
||||
"embedding_model": "Embedding Model",
|
||||
"please_select_embedding_model": "Please select an embedding model",
|
||||
"select_embedding_model_placeholder": "Select Embedding Model",
|
||||
"embedding_dimensions": "Embedding Dimensions",
|
||||
"stored_memories": "Stored Memories",
|
||||
"global_memory_description": "To use memory features, please enable global memory in assistant settings."
|
||||
},
|
||||
"trace": {
|
||||
"label": "Call Chain",
|
||||
"traceWindow": "Call Chain Window",
|
||||
"backList": "Back To List",
|
||||
"spanDetail": "Span Details",
|
||||
"name": "Node Name",
|
||||
"tag": "Tag",
|
||||
"startTime": "Start Time",
|
||||
"endTime": "End Time",
|
||||
"tokenUsage": "Token Usage",
|
||||
"spendTime": "Spend Time",
|
||||
"parentId": "Parent Id",
|
||||
"inputs": "Inputs",
|
||||
"outputs": "Outputs",
|
||||
"noTraceList": "No trace information found",
|
||||
"edasSupport": "Powered by Alibaba Cloud EDAS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2561,9 +2561,132 @@
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "ナレッジグラフ",
|
||||
"quit": "[to be translated]:退出",
|
||||
"quit": "終了",
|
||||
"show_window": "ウィンドウを表示",
|
||||
"visualization": "可視化"
|
||||
},
|
||||
"trace": {
|
||||
"label": "呼び出しチェーン",
|
||||
"traceWindow": "呼び出しチェーンウィンドウ",
|
||||
"backList": "リストに戻る",
|
||||
"spanDetail": "スパンの詳細",
|
||||
"name": "ノード名",
|
||||
"tag": "Tagラベル",
|
||||
"startTime": "開始時間",
|
||||
"endTime": "終了時間",
|
||||
"tokenUsage": "トークンの使用",
|
||||
"spendTime": "時間を過ごす",
|
||||
"parentId": "親ID",
|
||||
"inputs": "入力",
|
||||
"outputs": "出力",
|
||||
"noTraceList": "トレース情報が見つかりません",
|
||||
"edasSupport": "Powered by Alibaba Cloud EDAS"
|
||||
},
|
||||
"memory": {
|
||||
"title": "グローバルメモリ",
|
||||
"add_memory": "メモリーを追加",
|
||||
"edit_memory": "メモリーを編集",
|
||||
"memory_content": "メモリー内容",
|
||||
"please_enter_memory": "メモリー内容を入力してください",
|
||||
"memory_placeholder": "メモリー内容を入力...",
|
||||
"user_id": "ユーザーID",
|
||||
"user_id_placeholder": "ユーザーIDを入力(オプション)",
|
||||
"load_failed": "メモリーの読み込みに失敗しました",
|
||||
"add_success": "メモリーが正常に追加されました",
|
||||
"add_failed": "メモリーの追加に失敗しました",
|
||||
"update_success": "メモリーが正常に更新されました",
|
||||
"update_failed": "メモリーの更新に失敗しました",
|
||||
"delete_success": "メモリーが正常に削除されました",
|
||||
"delete_failed": "メモリーの削除に失敗しました",
|
||||
"delete_confirm_title": "メモリーを削除",
|
||||
"delete_confirm_content": "{{count}}件のメモリーを削除してもよろしいですか?",
|
||||
"delete_confirm": "このメモリーを削除してもよろしいですか?",
|
||||
"time": "時間",
|
||||
"user": "ユーザー",
|
||||
"content": "内容",
|
||||
"score": "スコア",
|
||||
"memories_description": "{{total}}件中{{count}}件のメモリーを表示",
|
||||
"search_placeholder": "メモリーを検索...",
|
||||
"start_date": "開始日",
|
||||
"end_date": "終了日",
|
||||
"all_users": "すべてのユーザー",
|
||||
"users": "ユーザー",
|
||||
"delete_selected": "選択したものを削除",
|
||||
"reset_filters": "フィルターをリセット",
|
||||
"pagination_total": "{{total}}件中{{start}}-{{end}}件",
|
||||
"current_user": "現在のユーザー",
|
||||
"select_user": "ユーザーを選択",
|
||||
"default_user": "デフォルトユーザー",
|
||||
"switch_user": "ユーザーを切り替え",
|
||||
"user_switched": "ユーザーコンテキストが{{user}}に切り替わりました",
|
||||
"switch_user_confirm": "ユーザーコンテキストを{{user}}に切り替えますか?",
|
||||
"add_user": "ユーザーを追加",
|
||||
"add_new_user": "新しいユーザーを追加",
|
||||
"new_user_id": "新しいユーザーID",
|
||||
"new_user_id_placeholder": "一意のユーザーIDを入力",
|
||||
"user_id_required": "ユーザーIDは必須です",
|
||||
"user_id_reserved": "'default-user'は予約済みです。別のIDを使用してください",
|
||||
"user_id_exists": "このユーザーIDはすでに存在します",
|
||||
"user_id_too_long": "ユーザーIDは50文字を超えられません",
|
||||
"user_id_invalid_chars": "ユーザーIDには文字、数字、ハイフン、アンダースコアのみ使用できます",
|
||||
"user_id_rules": "ユーザーIDは一意であり、文字、数字、ハイフン(-)、アンダースコア(_)のみ含む必要があります",
|
||||
"user_created": "ユーザー{{user}}が作成され、切り替えが成功しました",
|
||||
"add_user_failed": "ユーザーの追加に失敗しました",
|
||||
"memory": "個のメモリ",
|
||||
"reset_user_memories": "ユーザーメモリをリセット",
|
||||
"reset_memories": "メモリをリセット",
|
||||
"delete_user": "ユーザーを削除",
|
||||
"loading_memories": "メモリを読み込み中...",
|
||||
"no_memories": "メモリがありません",
|
||||
"no_matching_memories": "一致するメモリが見つかりません",
|
||||
"no_memories_description": "最初のメモリを追加してください",
|
||||
"try_different_filters": "検索条件を調整してください",
|
||||
"add_first_memory": "最初のメモリを追加",
|
||||
"user_switch_failed": "ユーザーの切り替えに失敗しました",
|
||||
"cannot_delete_default_user": "デフォルトユーザーは削除できません",
|
||||
"delete_user_confirm_title": "ユーザーを削除",
|
||||
"delete_user_confirm_content": "ユーザー{{user}}とそのすべてのメモリを削除してもよろしいですか?",
|
||||
"user_deleted": "ユーザー{{user}}が正常に削除されました",
|
||||
"delete_user_failed": "ユーザーの削除に失敗しました",
|
||||
"reset_user_memories_confirm_title": "ユーザーメモリをリセット",
|
||||
"reset_user_memories_confirm_content": "{{user}}のすべてのメモリをリセットしてもよろしいですか?",
|
||||
"user_memories_reset": "{{user}}のすべてのメモリがリセットされました",
|
||||
"reset_user_memories_failed": "ユーザーメモリのリセットに失敗しました",
|
||||
"reset_memories_confirm_title": "すべてのメモリをリセット",
|
||||
"reset_memories_confirm_content": "{{user}}のすべてのメモリを完全に削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"memories_reset_success": "{{user}}のすべてのメモリが正常にリセットされました",
|
||||
"reset_memories_failed": "メモリのリセットに失敗しました",
|
||||
"delete_confirm_single": "このメモリを削除してもよろしいですか?",
|
||||
"total_memories": "個のメモリ",
|
||||
"default": "デフォルト",
|
||||
"custom": "カスタム",
|
||||
"description": "メモリは、アシスタントとのやりとりに関する情報を保存・管理する機能です。メモリの追加、編集、削除のほか、フィルタリングや検索を行うことができます。",
|
||||
"global_memory_enabled": "グローバルメモリが有効化されました",
|
||||
"global_memory": "グローバルメモリ",
|
||||
"enable_global_memory_first": "最初にグローバルメモリを有効にしてください",
|
||||
"configure_memory_first": "最初にメモリ設定を構成してください",
|
||||
"global_memory_disabled_title": "グローバルメモリが無効です",
|
||||
"global_memory_disabled_desc": "メモリ機能を使用するには、まずアシスタント設定でグローバルメモリを有効にしてください。",
|
||||
"not_configured_title": "メモリが設定されていません",
|
||||
"not_configured_desc": "メモリ機能を有効にするには、メモリ設定で埋め込みとLLMモデルを設定してください。",
|
||||
"go_to_memory_page": "メモリページに移動",
|
||||
"settings": "設定",
|
||||
"statistics": "統計",
|
||||
"search": "検索",
|
||||
"actions": "アクション",
|
||||
"user_management": "ユーザー管理",
|
||||
"initial_memory_content": "ようこそ!これはあなたの最初の記憶です。",
|
||||
"loading": "思い出を読み込み中...",
|
||||
"settings_title": "メモリ設定",
|
||||
"llm_model": "LLMモデル",
|
||||
"please_select_llm_model": "LLMモデルを選択してください",
|
||||
"select_llm_model_placeholder": "LLMモデルを選択",
|
||||
"embedding_model": "埋め込みモデル",
|
||||
"please_select_embedding_model": "埋め込みモデルを選択してください",
|
||||
"select_embedding_model_placeholder": "埋め込みモデルを選択",
|
||||
"embedding_dimensions": "埋め込み次元",
|
||||
"stored_memories": "保存された記憶",
|
||||
"global_memory_description": "メモリ機能を使用するには、アシスタント設定でグローバルメモリを有効にしてください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2561,9 +2561,132 @@
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "Граф знаний",
|
||||
"quit": "[to be translated]:退出",
|
||||
"quit": "Выйти",
|
||||
"show_window": "Показать окно",
|
||||
"visualization": "Визуализация"
|
||||
},
|
||||
"memory": {
|
||||
"title": "Глобальная память",
|
||||
"add_memory": "Добавить память",
|
||||
"edit_memory": "Редактировать память",
|
||||
"memory_content": "Содержимое памяти",
|
||||
"please_enter_memory": "Пожалуйста, введите содержимое памяти",
|
||||
"memory_placeholder": "Введите содержимое памяти...",
|
||||
"user_id": "ID пользователя",
|
||||
"user_id_placeholder": "Введите ID пользователя (необязательно)",
|
||||
"load_failed": "Не удалось загрузить память",
|
||||
"add_success": "Память успешно добавлена",
|
||||
"add_failed": "Не удалось добавить память",
|
||||
"update_success": "Память успешно обновлена",
|
||||
"update_failed": "Не удалось обновить память",
|
||||
"delete_success": "Память успешно удалена",
|
||||
"delete_failed": "Не удалось удалить память",
|
||||
"delete_confirm_title": "Удалить память",
|
||||
"delete_confirm_content": "Вы уверены, что хотите удалить {{count}} записей памяти?",
|
||||
"delete_confirm": "Вы уверены, что хотите удалить эту запись памяти?",
|
||||
"time": "Время",
|
||||
"user": "Пользователь",
|
||||
"content": "Содержимое",
|
||||
"score": "Оценка",
|
||||
"memories_description": "Показано {{count}} из {{total}} записей памяти",
|
||||
"search_placeholder": "Поиск памяти...",
|
||||
"start_date": "Дата начала",
|
||||
"end_date": "Дата окончания",
|
||||
"all_users": "Все пользователи",
|
||||
"users": "пользователи",
|
||||
"delete_selected": "Удалить выбранные",
|
||||
"reset_filters": "Сбросить фильтры",
|
||||
"pagination_total": "{{start}}-{{end}} из {{total}} элементов",
|
||||
"current_user": "Текущий пользователь",
|
||||
"select_user": "Выбрать пользователя",
|
||||
"default_user": "Пользователь по умолчанию",
|
||||
"switch_user": "Переключить пользователя",
|
||||
"user_switched": "Контекст пользователя переключен на {{user}}",
|
||||
"switch_user_confirm": "Переключить контекст пользователя на {{user}}?",
|
||||
"add_user": "Добавить пользователя",
|
||||
"add_new_user": "Добавить нового пользователя",
|
||||
"new_user_id": "Новый ID пользователя",
|
||||
"new_user_id_placeholder": "Введите уникальный ID пользователя",
|
||||
"user_id_required": "ID пользователя обязателен",
|
||||
"user_id_reserved": "'default-user' зарезервирован, используйте другой ID",
|
||||
"user_id_exists": "Этот ID пользователя уже существует",
|
||||
"user_id_too_long": "ID пользователя не может превышать 50 символов",
|
||||
"user_id_invalid_chars": "ID пользователя может содержать только буквы, цифры, дефисы и подчёркивания",
|
||||
"user_id_rules": "ID пользователя должен быть уникальным и содержать только буквы, цифры, дефисы (-) и подчёркивания (_)",
|
||||
"user_created": "Пользователь {{user}} создан и переключен успешно",
|
||||
"add_user_failed": "Не удалось добавить пользователя",
|
||||
"memory": "воспоминаний",
|
||||
"reset_user_memories": "Сбросить воспоминания пользователя",
|
||||
"reset_memories": "Сбросить воспоминания",
|
||||
"delete_user": "Удалить пользователя",
|
||||
"loading_memories": "Загрузка воспоминаний...",
|
||||
"no_memories": "Нет воспоминаний",
|
||||
"no_matching_memories": "Подходящие воспоминания не найдены",
|
||||
"no_memories_description": "Начните с добавления вашего первого воспоминания",
|
||||
"try_different_filters": "Попробуйте изменить критерии поиска",
|
||||
"add_first_memory": "Добавить первое воспоминание",
|
||||
"user_switch_failed": "Не удалось переключить пользователя",
|
||||
"cannot_delete_default_user": "Нельзя удалить пользователя по умолчанию",
|
||||
"delete_user_confirm_title": "Удалить пользователя",
|
||||
"delete_user_confirm_content": "Вы уверены, что хотите удалить пользователя {{user}} и все его воспоминания?",
|
||||
"user_deleted": "Пользователь {{user}} успешно удален",
|
||||
"delete_user_failed": "Не удалось удалить пользователя",
|
||||
"reset_user_memories_confirm_title": "Сбросить воспоминания пользователя",
|
||||
"reset_user_memories_confirm_content": "Вы уверены, что хотите сбросить все воспоминания пользователя {{user}}?",
|
||||
"user_memories_reset": "Все воспоминания пользователя {{user}} сброшены",
|
||||
"reset_user_memories_failed": "Не удалось сбросить воспоминания пользователя",
|
||||
"reset_memories_confirm_title": "Сбросить все воспоминания",
|
||||
"reset_memories_confirm_content": "Вы уверены, что хотите навсегда удалить все воспоминания пользователя {{user}}? Это действие нельзя отменить.",
|
||||
"memories_reset_success": "Все воспоминания пользователя {{user}} успешно сброшены",
|
||||
"reset_memories_failed": "Не удалось сбросить воспоминания",
|
||||
"delete_confirm_single": "Вы уверены, что хотите удалить это воспоминание?",
|
||||
"total_memories": "всего воспоминаний",
|
||||
"default": "По умолчанию",
|
||||
"custom": "Пользовательский",
|
||||
"description": "Память позволяет хранить и управлять информацией о ваших взаимодействиях с ассистентом. Вы можете добавлять, редактировать и удалять воспоминания, а также фильтровать и искать их.",
|
||||
"global_memory_enabled": "Глобальная память включена",
|
||||
"global_memory": "Глобальная память",
|
||||
"enable_global_memory_first": "Сначала включите глобальную память",
|
||||
"configure_memory_first": "Сначала настройте параметры памяти",
|
||||
"global_memory_disabled_title": "Глобальная память отключена",
|
||||
"global_memory_disabled_desc": "Чтобы использовать функции памяти, сначала включите глобальную память в настройках ассистента.",
|
||||
"not_configured_title": "Память не настроена",
|
||||
"not_configured_desc": "Пожалуйста, настройте модели встраивания и LLM в настройках памяти, чтобы включить функциональность памяти.",
|
||||
"go_to_memory_page": "Перейти на страницу памяти",
|
||||
"settings": "Настройки",
|
||||
"statistics": "Статистика",
|
||||
"search": "Поиск",
|
||||
"actions": "Действия",
|
||||
"user_management": "Управление пользователями",
|
||||
"initial_memory_content": "Добро пожаловать! Это ваше первое воспоминание.",
|
||||
"loading": "Загрузка воспоминаний...",
|
||||
"settings_title": "Настройки памяти",
|
||||
"llm_model": "Модель LLM",
|
||||
"please_select_llm_model": "Пожалуйста, выберите модель LLM",
|
||||
"select_llm_model_placeholder": "Выбор модели LLM",
|
||||
"embedding_model": "Модель встраивания",
|
||||
"please_select_embedding_model": "Пожалуйста, выберите модель для внедрения",
|
||||
"select_embedding_model_placeholder": "Выберите модель внедрения",
|
||||
"embedding_dimensions": "Размерность вложения",
|
||||
"stored_memories": "Запасённые воспоминания",
|
||||
"global_memory_description": "Для использования функций памяти необходимо включить глобальную память в настройках ассистента."
|
||||
},
|
||||
"trace": {
|
||||
"label": "Цепочка вызовов",
|
||||
"traceWindow": "Окно цепочки вызовов",
|
||||
"backList": "Вернуться к списку",
|
||||
"spanDetail": "Span Подробнее",
|
||||
"name": "Имя узла",
|
||||
"tag": "ярлык",
|
||||
"startTime": "время начала",
|
||||
"endTime": "время окончания",
|
||||
"tokenUsage": "Использование токена",
|
||||
"spendTime": "тратитьВремя",
|
||||
"parentId": "Родительский идентификатор",
|
||||
"inputs": "входы",
|
||||
"outputs": "выходы",
|
||||
"noTraceList": "Информация о следах не найдена",
|
||||
"edasSupport": "Powered by Alibaba Cloud EDAS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2564,6 +2564,129 @@
|
||||
"quit": "退出",
|
||||
"show_window": "显示窗口",
|
||||
"visualization": "可视化"
|
||||
},
|
||||
"memory": {
|
||||
"title": "全局记忆",
|
||||
"settings": "设置",
|
||||
"statistics": "统计",
|
||||
"search": "搜索",
|
||||
"actions": "操作",
|
||||
"add_memory": "添加记忆",
|
||||
"edit_memory": "编辑记忆",
|
||||
"memory_content": "记忆内容",
|
||||
"please_enter_memory": "请输入记忆内容",
|
||||
"memory_placeholder": "输入记忆内容...",
|
||||
"user_id": "用户 ID",
|
||||
"user_id_placeholder": "输入用户 ID(可选)",
|
||||
"load_failed": "加载记忆失败",
|
||||
"add_success": "记忆添加成功",
|
||||
"add_failed": "添加记忆失败",
|
||||
"update_success": "记忆更新成功",
|
||||
"update_failed": "更新记忆失败",
|
||||
"delete_success": "记忆删除成功",
|
||||
"delete_failed": "删除记忆失败",
|
||||
"delete_confirm_title": "删除记忆",
|
||||
"delete_confirm_content": "确定要删除 {{count}} 条记忆吗?",
|
||||
"delete_confirm": "确定要删除这条记忆吗?",
|
||||
"time": "时间",
|
||||
"user": "用户",
|
||||
"content": "内容",
|
||||
"score": "分数",
|
||||
"memories_description": "显示 {{count}} / {{total}} 条记忆",
|
||||
"search_placeholder": "搜索记忆...",
|
||||
"start_date": "开始日期",
|
||||
"end_date": "结束日期",
|
||||
"all_users": "所有用户",
|
||||
"users": "用户",
|
||||
"delete_selected": "删除选中",
|
||||
"reset_filters": "重置筛选",
|
||||
"pagination_total": "第 {{start}}-{{end}} 项,共 {{total}} 项",
|
||||
"current_user": "当前用户",
|
||||
"select_user": "选择用户",
|
||||
"default_user": "默认用户",
|
||||
"switch_user": "切换用户",
|
||||
"user_switched": "用户上下文已切换到 {{user}}",
|
||||
"switch_user_confirm": "将用户上下文切换到 {{user}}?",
|
||||
"add_user": "添加用户",
|
||||
"add_new_user": "添加新用户",
|
||||
"new_user_id": "新用户ID",
|
||||
"new_user_id_placeholder": "输入唯一的用户ID",
|
||||
"user_management": "用户管理",
|
||||
"user_id_required": "用户ID为必填项",
|
||||
"user_id_reserved": "'default-user' 为保留字,请使用其他ID",
|
||||
"user_id_exists": "该用户ID已存在",
|
||||
"user_id_too_long": "用户ID不能超过50个字符",
|
||||
"user_id_invalid_chars": "用户ID只能包含字母、数字、连字符和下划线",
|
||||
"user_id_rules": "用户ID必须唯一,只能包含字母、数字、连字符(-)和下划线(_)",
|
||||
"user_created": "用户 {{user}} 创建并切换成功",
|
||||
"add_user_failed": "添加用户失败",
|
||||
"memory": "条记忆",
|
||||
"reset_user_memories": "重置用户记忆",
|
||||
"reset_memories": "重置记忆",
|
||||
"delete_user": "删除用户",
|
||||
"loading_memories": "正在加载记忆...",
|
||||
"no_memories": "暂无记忆",
|
||||
"no_matching_memories": "未找到匹配的记忆",
|
||||
"no_memories_description": "开始添加您的第一条记忆吧",
|
||||
"try_different_filters": "尝试调整搜索条件",
|
||||
"add_first_memory": "添加您的第一条记忆",
|
||||
"user_switch_failed": "切换用户失败",
|
||||
"cannot_delete_default_user": "不能删除默认用户",
|
||||
"delete_user_confirm_title": "删除用户",
|
||||
"delete_user_confirm_content": "确定要删除用户 {{user}} 及其所有记忆吗?",
|
||||
"user_deleted": "用户 {{user}} 删除成功",
|
||||
"delete_user_failed": "删除用户失败",
|
||||
"reset_user_memories_confirm_title": "重置用户记忆",
|
||||
"reset_user_memories_confirm_content": "确定要重置 {{user}} 的所有记忆吗?",
|
||||
"user_memories_reset": "{{user}} 的所有记忆已重置",
|
||||
"reset_user_memories_failed": "重置用户记忆失败",
|
||||
"reset_memories_confirm_title": "重置所有记忆",
|
||||
"reset_memories_confirm_content": "确定要永久删除 {{user}} 的所有记忆吗?此操作无法撤销。",
|
||||
"memories_reset_success": "{{user}} 的所有记忆已成功重置",
|
||||
"reset_memories_failed": "重置记忆失败",
|
||||
"delete_confirm_single": "确定要删除这条记忆吗?",
|
||||
"total_memories": "条记忆",
|
||||
"default": "默认",
|
||||
"custom": "自定义",
|
||||
"description": "记忆功能允许您存储和管理与助手交互的信息。您可以添加、编辑和删除记忆,也可以对它们进行过滤和搜索。",
|
||||
"global_memory_enabled": "全局记忆已启用",
|
||||
"global_memory": "全局记忆",
|
||||
"enable_global_memory_first": "请先启用全局记忆",
|
||||
"configure_memory_first": "请先配置记忆设置",
|
||||
"global_memory_disabled_title": "全局记忆已禁用",
|
||||
"global_memory_disabled_desc": "要使用记忆功能,请先在助手设置中启用全局记忆。",
|
||||
"not_configured_title": "记忆未配置",
|
||||
"not_configured_desc": "请在记忆设置中配置嵌入和LLM模型以启用记忆功能。",
|
||||
"go_to_memory_page": "前往记忆页面",
|
||||
"initial_memory_content": "欢迎!这是您的第一条记忆。",
|
||||
"loading": "正在加载记忆...",
|
||||
"settings_title": "记忆设置",
|
||||
"llm_model": "LLM 模型",
|
||||
"please_select_llm_model": "请选择 LLM 模型",
|
||||
"select_llm_model_placeholder": "选择 LLM 模型",
|
||||
"embedding_model": "嵌入模型",
|
||||
"please_select_embedding_model": "请选择嵌入模型",
|
||||
"select_embedding_model_placeholder": "选择嵌入模型",
|
||||
"embedding_dimensions": "嵌入维度",
|
||||
"stored_memories": "已存储记忆",
|
||||
"global_memory_description": "需要开启助手设置中的全局记忆才能使用"
|
||||
},
|
||||
"trace": {
|
||||
"label": "调用链",
|
||||
"traceWindow": "调用链窗口",
|
||||
"backList": "返回列表",
|
||||
"spanDetail": "Span详情",
|
||||
"name": "节点名称",
|
||||
"tag": "标签",
|
||||
"startTime": "开始时间",
|
||||
"endTime": "结束时间",
|
||||
"tokenUsage": "Token使用量",
|
||||
"spendTime": "消耗时间",
|
||||
"parentId": "上级Id",
|
||||
"inputs": "输入",
|
||||
"outputs": "输出",
|
||||
"noTraceList": "没有找到Trace信息",
|
||||
"edasSupport": "Powered by Alibaba Cloud EDAS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2561,9 +2561,132 @@
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "知識圖譜",
|
||||
"quit": "[to be translated]:退出",
|
||||
"quit": "結束",
|
||||
"show_window": "顯示視窗",
|
||||
"visualization": "視覺化"
|
||||
},
|
||||
"memory": {
|
||||
"title": "全域記憶",
|
||||
"add_memory": "新增記憶",
|
||||
"edit_memory": "編輯記憶",
|
||||
"memory_content": "記憶內容",
|
||||
"please_enter_memory": "請輸入記憶內容",
|
||||
"memory_placeholder": "輸入記憶內容...",
|
||||
"user_id": "使用者ID",
|
||||
"user_id_placeholder": "輸入使用者ID(可選)",
|
||||
"load_failed": "載入記憶失敗",
|
||||
"add_success": "記憶新增成功",
|
||||
"add_failed": "新增記憶失敗",
|
||||
"update_success": "記憶更新成功",
|
||||
"update_failed": "更新記憶失敗",
|
||||
"delete_success": "記憶刪除成功",
|
||||
"delete_failed": "刪除記憶失敗",
|
||||
"delete_confirm_title": "刪除記憶",
|
||||
"delete_confirm_content": "確定要刪除 {{count}} 條記憶嗎?",
|
||||
"delete_confirm": "確定要刪除這條記憶嗎?",
|
||||
"time": "時間",
|
||||
"user": "使用者",
|
||||
"content": "內容",
|
||||
"score": "分數",
|
||||
"memories_description": "顯示 {{count}} / {{total}} 條記憶",
|
||||
"search_placeholder": "搜尋記憶...",
|
||||
"start_date": "開始日期",
|
||||
"end_date": "結束日期",
|
||||
"all_users": "所有使用者",
|
||||
"users": "使用者",
|
||||
"delete_selected": "刪除選取",
|
||||
"reset_filters": "重設篩選",
|
||||
"pagination_total": "第 {{start}}-{{end}} 項,共 {{total}} 項",
|
||||
"current_user": "目前使用者",
|
||||
"select_user": "選擇使用者",
|
||||
"default_user": "預設使用者",
|
||||
"switch_user": "切換使用者",
|
||||
"user_switched": "使用者內容已切換至 {{user}}",
|
||||
"switch_user_confirm": "將使用者內容切換至 {{user}}?",
|
||||
"add_user": "新增使用者",
|
||||
"add_new_user": "新增新使用者",
|
||||
"new_user_id": "新使用者ID",
|
||||
"new_user_id_placeholder": "輸入唯一的使用者ID",
|
||||
"user_id_required": "使用者ID為必填欄位",
|
||||
"user_id_reserved": "'default-user' 為保留字,請使用其他ID",
|
||||
"user_id_exists": "此使用者ID已存在",
|
||||
"user_id_too_long": "使用者ID不能超過50個字元",
|
||||
"user_id_invalid_chars": "使用者ID只能包含字母、數字、連字符和底線",
|
||||
"user_id_rules": "使用者ID必须唯一,只能包含字母、數字、連字符(-)和底線(_)",
|
||||
"user_created": "使用者 {{user}} 建立並切換成功",
|
||||
"add_user_failed": "新增使用者失敗",
|
||||
"memory": "個記憶",
|
||||
"reset_user_memories": "重置使用者記憶",
|
||||
"reset_memories": "重置記憶",
|
||||
"delete_user": "刪除使用者",
|
||||
"loading_memories": "正在載入記憶...",
|
||||
"no_memories": "暫無記憶",
|
||||
"no_matching_memories": "未找到符合的記憶",
|
||||
"no_memories_description": "開始新增您的第一個記憶吧",
|
||||
"try_different_filters": "嘗試調整搜尋條件",
|
||||
"add_first_memory": "新增您的第一個記憶",
|
||||
"user_switch_failed": "切換使用者失敗",
|
||||
"cannot_delete_default_user": "不能刪除預設使用者",
|
||||
"delete_user_confirm_title": "刪除使用者",
|
||||
"delete_user_confirm_content": "確定要刪除使用者 {{user}} 及其所有記憶嗎?",
|
||||
"user_deleted": "使用者 {{user}} 刪除成功",
|
||||
"delete_user_failed": "刪除使用者失敗",
|
||||
"reset_user_memories_confirm_title": "重置使用者記憶",
|
||||
"reset_user_memories_confirm_content": "確定要重置 {{user}} 的所有記憶嗎?",
|
||||
"user_memories_reset": "{{user}} 的所有記憶已重置",
|
||||
"reset_user_memories_failed": "重置使用者記憶失敗",
|
||||
"reset_memories_confirm_title": "重置所有記憶",
|
||||
"reset_memories_confirm_content": "確定要永久刪除 {{user}} 的所有記憶嗎?此操作無法復原。",
|
||||
"memories_reset_success": "{{user}} 的所有記憶已成功重置",
|
||||
"reset_memories_failed": "重置記憶失敗",
|
||||
"delete_confirm_single": "確定要刪除這個記憶嗎?",
|
||||
"total_memories": "個記憶",
|
||||
"default": "預設",
|
||||
"custom": "自定義",
|
||||
"description": "記憶功能讓您儲存和管理與助手互動的資訊。您可以新增、編輯和刪除記憶,也可以對它們進行篩選和搜尋。",
|
||||
"global_memory_enabled": "全域記憶已啟用",
|
||||
"global_memory": "全域記憶",
|
||||
"enable_global_memory_first": "請先啟用全域記憶",
|
||||
"configure_memory_first": "請先配置記憶設定",
|
||||
"global_memory_disabled_title": "全域記憶已停用",
|
||||
"global_memory_disabled_desc": "要使用記憶功能,請先在助手設定中啟用全域記憶。",
|
||||
"not_configured_title": "記憶未配置",
|
||||
"not_configured_desc": "請在記憶設定中配置嵌入和LLM模型以啟用記憶功能。",
|
||||
"go_to_memory_page": "前往記憶頁面",
|
||||
"settings": "設定",
|
||||
"statistics": "統計",
|
||||
"search": "搜尋",
|
||||
"actions": "操作",
|
||||
"user_management": "使用者管理",
|
||||
"initial_memory_content": "歡迎!這是你的第一個記憶。",
|
||||
"loading": "載入記憶中...",
|
||||
"settings_title": "記憶體設定",
|
||||
"llm_model": "LLM 模型",
|
||||
"please_select_llm_model": "請選擇一個LLM模型",
|
||||
"select_llm_model_placeholder": "選擇LLM模型",
|
||||
"embedding_model": "嵌入模型",
|
||||
"please_select_embedding_model": "請選擇一個嵌入模型",
|
||||
"select_embedding_model_placeholder": "選擇嵌入模型",
|
||||
"embedding_dimensions": "嵌入維度",
|
||||
"stored_memories": "儲存的記憶",
|
||||
"global_memory_description": "需要開啟助手設定中的全域記憶才能使用"
|
||||
},
|
||||
"trace": {
|
||||
"label": "呼叫鏈",
|
||||
"traceWindow": "呼叫鏈視窗",
|
||||
"backList": "返回清單",
|
||||
"spanDetail": "Span詳情",
|
||||
"name": "節點名稱",
|
||||
"tag": "標籤",
|
||||
"startTime": "開始時間",
|
||||
"endTime": "結束時間",
|
||||
"tokenUsage": "Token使用量",
|
||||
"spendTime": "消耗時間",
|
||||
"parentId": "上級Id",
|
||||
"inputs": "輸入",
|
||||
"outputs": "輸出",
|
||||
"noTraceList": "沒有找到Trace資訊",
|
||||
"edasSupport": "Powered by Alibaba Cloud EDAS"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -341,6 +341,7 @@
|
||||
"provider": "Παρέχων",
|
||||
"reasoning_content": "Έχει σκεφτεί πολύ καλά",
|
||||
"regenerate": "Ξαναπαραγωγή",
|
||||
"trace": "ίχνος",
|
||||
"rename": "Μετονομασία",
|
||||
"reset": "Επαναφορά",
|
||||
"save": "Αποθήκευση",
|
||||
|
||||
@ -342,6 +342,7 @@
|
||||
"provider": "Proveedor",
|
||||
"reasoning_content": "Pensamiento profundo",
|
||||
"regenerate": "Regenerar",
|
||||
"trace": "Rastro",
|
||||
"rename": "Renombrar",
|
||||
"reset": "Restablecer",
|
||||
"save": "Guardar",
|
||||
|
||||
@ -341,6 +341,7 @@
|
||||
"provider": "Fournisseur",
|
||||
"reasoning_content": "Réflexion approfondie",
|
||||
"regenerate": "Regénérer",
|
||||
"trace": "Tracer",
|
||||
"rename": "Renommer",
|
||||
"reset": "Réinitialiser",
|
||||
"save": "Enregistrer",
|
||||
|
||||
@ -343,6 +343,7 @@
|
||||
"provider": "Fornecedor",
|
||||
"reasoning_content": "Pensamento profundo concluído",
|
||||
"regenerate": "Regenerar",
|
||||
"trace": "Regenerar",
|
||||
"rename": "Renomear",
|
||||
"reset": "Redefinir",
|
||||
"save": "Salvar",
|
||||
|
||||
@ -4,6 +4,7 @@ import { loggerService } from '@logger'
|
||||
import { startAutoSync } from './services/BackupService'
|
||||
import { startNutstoreAutoSync } from './services/NutstoreService'
|
||||
import storeSyncService from './services/StoreSyncService'
|
||||
import { webTraceService } from './services/WebTraceService'
|
||||
import store from './store'
|
||||
|
||||
loggerService.initWindowSource('mainWindow')
|
||||
@ -30,6 +31,11 @@ function initStoreSync() {
|
||||
storeSyncService.subscribe()
|
||||
}
|
||||
|
||||
function initWebTrace() {
|
||||
webTraceService.init()
|
||||
}
|
||||
|
||||
initKeyv()
|
||||
initAutoSync()
|
||||
initStoreSync()
|
||||
initWebTrace()
|
||||
|
||||
@ -26,6 +26,7 @@ import FileManager from '@renderer/services/FileManager'
|
||||
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import PasteService from '@renderer/services/PasteService'
|
||||
import { spanManagerService } from '@renderer/services/SpanManagerService'
|
||||
import { estimateTextTokens as estimateTxtTokens, estimateUserPromptUsage } from '@renderer/services/TokenService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
@ -209,7 +210,11 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
|
||||
logger.info('Starting to send message')
|
||||
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE)
|
||||
const parent = spanManagerService.startTrace(
|
||||
{ topicId: topic.id, name: 'sendMessage', inputs: text },
|
||||
mentionedModels && mentionedModels.length > 0 ? mentionedModels : [assistant.model]
|
||||
)
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, { topicId: topic.id, traceId: parent?.spanContext().traceId })
|
||||
|
||||
try {
|
||||
// Dispatch the sendMessage action with all options
|
||||
@ -234,6 +239,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
baseUserMessage.usage = await estimateUserPromptUsage(baseUserMessage)
|
||||
|
||||
const { message, blocks } = getUserMessage(baseUserMessage)
|
||||
message.traceId = parent?.spanContext().traceId
|
||||
|
||||
currentMessageId.current = message.id
|
||||
dispatch(_sendMessage(message, blocks, assistantWithTopicPrompt, topic.id))
|
||||
@ -246,6 +252,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
setExpend(false)
|
||||
} catch (error) {
|
||||
logger.warn('Failed to send message:', error)
|
||||
parent?.recordException(error as Error)
|
||||
}
|
||||
}, [assistant, dispatch, files, inputEmpty, loading, mentionedModels, resizeTextArea, text, topic])
|
||||
|
||||
@ -472,7 +479,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
await onPause()
|
||||
await delay(1)
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
|
||||
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES, topic)
|
||||
}
|
||||
|
||||
const onNewContext = () => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { defaultConfig } from '@mcp-trace/trace-core'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
@ -14,6 +15,7 @@ import { translateText } from '@renderer/services/TranslateService'
|
||||
import store, { RootState } from '@renderer/store'
|
||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { TraceIcon } from '@renderer/trace/pages/Component'
|
||||
import type { Assistant, Language, Model, Topic } from '@renderer/types'
|
||||
import { type Message, MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL, classNames } from '@renderer/utils'
|
||||
@ -45,7 +47,7 @@ import {
|
||||
ThumbsUp,
|
||||
Trash
|
||||
} from 'lucide-react'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
@ -177,6 +179,24 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
[isTranslating, message, getTranslationUpdater, mainTextContent]
|
||||
)
|
||||
|
||||
const [isDevelopModel, setIsDevelopModel] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
setIsDevelopModel(defaultConfig.isDevModel || false)
|
||||
}, [])
|
||||
|
||||
const handleTraceUserMessage = useCallback(async () => {
|
||||
console.log('current traceId', message.traceId, 'start send')
|
||||
if (message.traceId) {
|
||||
window.api.trace.openWindow(
|
||||
message.topicId,
|
||||
message.traceId,
|
||||
true,
|
||||
message.role === 'user' ? undefined : message.model?.name
|
||||
)
|
||||
}
|
||||
}, [message])
|
||||
|
||||
const isEditable = useMemo(() => {
|
||||
return findMainTextBlocks(message).length > 0 // 使用 MCP Server 后会有大于一段 MatinTextBlock
|
||||
}, [message])
|
||||
@ -560,7 +580,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
|
||||
onConfirm={() => deleteMessage(message.id)}>
|
||||
onConfirm={() => deleteMessage(message.id, message.traceId, message.model?.name)}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@ -574,6 +594,13 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</Popconfirm>
|
||||
{isDevelopModel && message.traceId && (
|
||||
<Tooltip title={t('trace.label')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={() => handleTraceUserMessage()}>
|
||||
<TraceIcon size={16} className={'lucide lucide-trash'} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isUserMessage && (
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
|
||||
|
||||
@ -161,6 +161,7 @@ const DataSettings: FC = () => {
|
||||
onOk: async () => {
|
||||
try {
|
||||
await window.api.clearCache()
|
||||
await window.api.trace.cleanLocalData()
|
||||
await window.api.getCacheSize().then(setCacheSize)
|
||||
window.message.success(t('settings.data.clear_cache.success'))
|
||||
} catch (error) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { WebSearchState } from '@renderer/store/websearch'
|
||||
import { withSpanResult } from '@renderer/services/SpanManagerService'
|
||||
import type { WebSearchState } from '@renderer/store/websearch'
|
||||
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
||||
import { filterResultWithBlacklist } from '@renderer/utils/blacklistMatchPattern'
|
||||
|
||||
@ -7,16 +8,38 @@ import WebSearchProviderFactory from './WebSearchProviderFactory'
|
||||
|
||||
export default class WebSearchEngineProvider {
|
||||
private sdk: BaseWebSearchProvider
|
||||
private providerName: string
|
||||
private topicId: string | undefined
|
||||
private parentSpanId: string | undefined
|
||||
private modelName: string | undefined
|
||||
|
||||
constructor(provider: WebSearchProvider) {
|
||||
constructor(provider: WebSearchProvider, parentSpanId?: string) {
|
||||
this.sdk = WebSearchProviderFactory.create(provider)
|
||||
this.providerName = provider.name
|
||||
this.topicId = provider.topicId
|
||||
this.parentSpanId = parentSpanId
|
||||
this.modelName = provider.modelName
|
||||
}
|
||||
|
||||
public async search(
|
||||
query: string,
|
||||
websearch: WebSearchState,
|
||||
httpOptions?: RequestInit
|
||||
): Promise<WebSearchProviderResponse> {
|
||||
const result = await this.sdk.search(query, websearch, httpOptions)
|
||||
const callSearch = async ({ query, websearch }) => {
|
||||
return await this.sdk.search(query, websearch, httpOptions)
|
||||
}
|
||||
|
||||
const traceParams = {
|
||||
name: `${this.providerName}.search`,
|
||||
tag: 'Web',
|
||||
topicId: this.topicId || '',
|
||||
parentSpanId: this.parentSpanId,
|
||||
modelName: this.modelName
|
||||
}
|
||||
|
||||
const result = await withSpanResult(callSearch, traceParams, { query, websearch })
|
||||
|
||||
return await filterResultWithBlacklist(result, websearch)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
import { getModel } from '@renderer/hooks/useModel'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { currentSpan, withSpanResult } from '@renderer/services/SpanManagerService'
|
||||
import store from '@renderer/store'
|
||||
import { selectCurrentUserId, selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory'
|
||||
import {
|
||||
@ -110,11 +111,24 @@ async function fetchExternalTool(
|
||||
summaryAssistant.model = assistant.model || getDefaultModel()
|
||||
summaryAssistant.prompt = prompt
|
||||
|
||||
const callSearchSummary = async (params: { messages: Message[]; assistant: Assistant }) => {
|
||||
return await fetchSearchSummary(params)
|
||||
}
|
||||
|
||||
const traceParams = {
|
||||
name: `${summaryAssistant.model?.name}.Summary`,
|
||||
tag: 'LLM',
|
||||
topicId: lastUserMessage.topicId,
|
||||
modelName: summaryAssistant.model.name
|
||||
}
|
||||
|
||||
const searchSummaryParams = {
|
||||
messages: lastAnswer ? [lastAnswer, lastUserMessage] : [lastUserMessage],
|
||||
assistant: summaryAssistant
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetchSearchSummary({
|
||||
messages: lastAnswer ? [lastAnswer, lastUserMessage] : [lastUserMessage],
|
||||
assistant: summaryAssistant
|
||||
})
|
||||
const result = await withSpanResult(callSearchSummary, traceParams, searchSummaryParams)
|
||||
|
||||
if (!result) return getFallbackResult()
|
||||
|
||||
@ -145,7 +159,10 @@ async function fetchExternalTool(
|
||||
}
|
||||
|
||||
// --- Web Search Function ---
|
||||
const searchTheWeb = async (extractResults: ExtractResults | undefined): Promise<WebSearchResponse | undefined> => {
|
||||
const searchTheWeb = async (
|
||||
extractResults: ExtractResults | undefined,
|
||||
parentSpanId?: string
|
||||
): Promise<WebSearchResponse | undefined> => {
|
||||
if (!shouldWebSearch) return
|
||||
|
||||
// Add check for extractResults existence early
|
||||
@ -165,8 +182,17 @@ async function fetchExternalTool(
|
||||
try {
|
||||
// Use the consolidated processWebsearch function
|
||||
WebSearchService.createAbortSignal(lastUserMessage.id)
|
||||
let safeWebSearchProvider = webSearchProvider
|
||||
if (webSearchProvider) {
|
||||
safeWebSearchProvider = {
|
||||
...webSearchProvider,
|
||||
topicId: lastUserMessage.topicId,
|
||||
parentSpanId,
|
||||
modelName: assistant.model.name
|
||||
}
|
||||
}
|
||||
const webSearchResponse = await WebSearchService.processWebsearch(
|
||||
webSearchProvider!,
|
||||
safeWebSearchProvider!,
|
||||
extractResults,
|
||||
lastUserMessage.id
|
||||
)
|
||||
@ -222,7 +248,9 @@ async function fetchExternalTool(
|
||||
|
||||
// --- Knowledge Base Search Function ---
|
||||
const searchKnowledgeBase = async (
|
||||
extractResults: ExtractResults | undefined
|
||||
extractResults: ExtractResults | undefined,
|
||||
parentSpanId?: string,
|
||||
modelName?: string
|
||||
): Promise<KnowledgeReference[] | undefined> => {
|
||||
if (!hasKnowledgeBase) return
|
||||
|
||||
@ -253,7 +281,13 @@ async function fetchExternalTool(
|
||||
// const mainTextBlock = mainTextBlocks
|
||||
// ?.map((blockId) => store.getState().messageBlocks.entities[blockId])
|
||||
// .find((block) => block?.type === MessageBlockType.MAIN_TEXT) as MainTextMessageBlock | undefined
|
||||
return await processKnowledgeSearch(tempExtractResults, knowledgeBaseIds)
|
||||
return await processKnowledgeSearch(
|
||||
tempExtractResults,
|
||||
knowledgeBaseIds,
|
||||
lastUserMessage.topicId,
|
||||
parentSpanId,
|
||||
modelName
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('Knowledge base search failed:', error)
|
||||
return
|
||||
@ -274,11 +308,12 @@ async function fetchExternalTool(
|
||||
let knowledgeReferencesFromSearch: KnowledgeReference[] | undefined
|
||||
let memorySearchReferences: MemoryItem[] | undefined
|
||||
|
||||
const parentSpanId = currentSpan(lastUserMessage.topicId, assistant.model?.name)?.spanContext().spanId
|
||||
// 并行执行搜索
|
||||
if (shouldWebSearch || shouldKnowledgeSearch || shouldSearchMemory) {
|
||||
;[webSearchResponseFromSearch, knowledgeReferencesFromSearch, memorySearchReferences] = await Promise.all([
|
||||
searchTheWeb(extractResults),
|
||||
searchKnowledgeBase(extractResults),
|
||||
searchTheWeb(extractResults, parentSpanId),
|
||||
searchKnowledgeBase(extractResults, parentSpanId, assistant.model?.name),
|
||||
searchMemory()
|
||||
])
|
||||
}
|
||||
@ -319,9 +354,10 @@ async function fetchExternalTool(
|
||||
|
||||
if (enabledMCPs && enabledMCPs.length > 0) {
|
||||
try {
|
||||
const spanContext = currentSpan(lastUserMessage.topicId, assistant.model?.name)?.spanContext()
|
||||
const toolPromises = enabledMCPs.map<Promise<MCPTool[]>>(async (mcpServer) => {
|
||||
try {
|
||||
const tools = await window.api.mcp.listTools(mcpServer)
|
||||
const tools = await window.api.mcp.listTools(mcpServer, spanContext)
|
||||
return tools.filter((tool: any) => !mcpServer.disabledTools?.includes(tool.name))
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching tools from MCP server ${mcpServer.name}:`, error)
|
||||
@ -417,24 +453,27 @@ export async function fetchChatCompletion({
|
||||
|
||||
// --- Call AI Completions ---
|
||||
onChunkReceived({ type: ChunkType.LLM_RESPONSE_CREATED })
|
||||
await AI.completions(
|
||||
{
|
||||
callType: 'chat',
|
||||
messages: _messages,
|
||||
assistant,
|
||||
onChunk: onChunkReceived,
|
||||
mcpTools: mcpTools,
|
||||
maxTokens,
|
||||
streamOutput: assistant.settings?.streamOutput || false,
|
||||
enableReasoning,
|
||||
enableWebSearch,
|
||||
enableUrlContext,
|
||||
enableGenerateImage
|
||||
},
|
||||
{
|
||||
streamOutput: assistant.settings?.streamOutput || false
|
||||
}
|
||||
)
|
||||
|
||||
const completionsParams: CompletionsParams = {
|
||||
callType: 'chat',
|
||||
messages: _messages,
|
||||
assistant,
|
||||
onChunk: onChunkReceived,
|
||||
mcpTools: mcpTools,
|
||||
maxTokens,
|
||||
streamOutput: assistant.settings?.streamOutput || false,
|
||||
enableReasoning,
|
||||
enableWebSearch,
|
||||
enableUrlContext,
|
||||
enableGenerateImage,
|
||||
topicId: lastUserMessage.topicId
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
streamOutput: assistant.settings?.streamOutput || false
|
||||
}
|
||||
|
||||
return await AI.completionsForTrace(completionsParams, requestOptions)
|
||||
|
||||
// Post-conversation memory processing
|
||||
const globalMemoryEnabled = selectGlobalMemoryEnabled(store.getState())
|
||||
@ -600,6 +639,8 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
|
||||
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
const topicId = messages?.find((message) => message.topicId)?.topicId || undefined
|
||||
|
||||
// LLM对多条消息的总结有问题,用单条结构化的消息表示会话内容会更好
|
||||
const structredMessages = contextMessages.map((message) => {
|
||||
const structredMessage = {
|
||||
@ -637,11 +678,12 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
|
||||
assistant: { ...summaryAssistant, prompt, model },
|
||||
maxTokens: 1000,
|
||||
streamOutput: false,
|
||||
topicId,
|
||||
enableReasoning: false
|
||||
}
|
||||
|
||||
try {
|
||||
const { getText } = await AI.completions(params)
|
||||
const { getText } = await AI.completionsForTrace(params)
|
||||
const text = getText()
|
||||
return removeSpecialCharactersForTopicName(text) || null
|
||||
} catch (error: any) {
|
||||
@ -657,16 +699,19 @@ export async function fetchSearchSummary({ messages, assistant }: { messages: Me
|
||||
return null
|
||||
}
|
||||
|
||||
const topicId = messages?.find((message) => message.topicId)?.topicId || undefined
|
||||
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
const params: CompletionsParams = {
|
||||
callType: 'search',
|
||||
messages: messages,
|
||||
assistant,
|
||||
streamOutput: false
|
||||
streamOutput: false,
|
||||
topicId
|
||||
}
|
||||
|
||||
return await AI.completions(params)
|
||||
return await AI.completionsForTrace(params)
|
||||
}
|
||||
|
||||
export async function fetchGenerate({
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { loggerService } from '@logger'
|
||||
import { Span } from '@opentelemetry/api'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@renderer/config/constant'
|
||||
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
|
||||
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import store from '@renderer/store'
|
||||
import { FileMetadata, KnowledgeBase, KnowledgeBaseParams, KnowledgeReference } from '@renderer/types'
|
||||
import { ExtractResults } from '@renderer/utils/extract'
|
||||
@ -102,18 +104,40 @@ export const getKnowledgeSourceUrl = async (item: ExtractChunkData & { file: Fil
|
||||
export const searchKnowledgeBase = async (
|
||||
query: string,
|
||||
base: KnowledgeBase,
|
||||
rewrite?: string
|
||||
rewrite?: string,
|
||||
topicId?: string,
|
||||
parentSpanId?: string,
|
||||
modelName?: string
|
||||
): Promise<Array<ExtractChunkData & { file: FileMetadata | null }>> => {
|
||||
let currentSpan: Span | undefined = undefined
|
||||
try {
|
||||
const baseParams = getKnowledgeBaseParams(base)
|
||||
const documentCount = base.documentCount || DEFAULT_KNOWLEDGE_DOCUMENT_COUNT
|
||||
const threshold = base.threshold || DEFAULT_KNOWLEDGE_THRESHOLD
|
||||
|
||||
if (topicId) {
|
||||
currentSpan = addSpan({
|
||||
topicId,
|
||||
name: `${base.name}-search`,
|
||||
inputs: {
|
||||
query,
|
||||
rewrite,
|
||||
base: baseParams
|
||||
},
|
||||
tag: 'Knowledge',
|
||||
parentSpanId,
|
||||
modelName
|
||||
})
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
const searchResults = await window.api.knowledgeBase.search({
|
||||
search: rewrite || query,
|
||||
base: baseParams
|
||||
})
|
||||
const searchResults = await window.api.knowledgeBase.search(
|
||||
{
|
||||
search: rewrite || query,
|
||||
base: baseParams
|
||||
},
|
||||
currentSpan?.spanContext()
|
||||
)
|
||||
|
||||
// 过滤阈值不达标的结果
|
||||
const filteredResults = searchResults.filter((item) => item.score >= threshold)
|
||||
@ -121,33 +145,56 @@ export const searchKnowledgeBase = async (
|
||||
// 如果有rerank模型,执行重排
|
||||
let rerankResults = filteredResults
|
||||
if (base.rerankModel && filteredResults.length > 0) {
|
||||
rerankResults = await window.api.knowledgeBase.rerank({
|
||||
search: rewrite || query,
|
||||
base: baseParams,
|
||||
results: filteredResults
|
||||
})
|
||||
rerankResults = await window.api.knowledgeBase.rerank(
|
||||
{
|
||||
search: rewrite || query,
|
||||
base: baseParams,
|
||||
results: filteredResults
|
||||
},
|
||||
currentSpan?.spanContext()
|
||||
)
|
||||
}
|
||||
|
||||
// 限制文档数量
|
||||
const limitedResults = rerankResults.slice(0, documentCount)
|
||||
|
||||
// 处理文件信息
|
||||
return await Promise.all(
|
||||
const result = await Promise.all(
|
||||
limitedResults.map(async (item) => {
|
||||
const file = await getFileFromUrl(item.metadata.source)
|
||||
logger.debug('Knowledge search item:', item, 'File:', file)
|
||||
return { ...item, file }
|
||||
})
|
||||
)
|
||||
if (topicId) {
|
||||
endSpan({
|
||||
topicId,
|
||||
outputs: result,
|
||||
span: currentSpan,
|
||||
modelName
|
||||
})
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error(`Error searching knowledge base ${base.name}:`, error)
|
||||
if (topicId) {
|
||||
endSpan({
|
||||
topicId,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
span: currentSpan,
|
||||
modelName
|
||||
})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const processKnowledgeSearch = async (
|
||||
extractResults: ExtractResults,
|
||||
knowledgeBaseIds: string[] | undefined
|
||||
knowledgeBaseIds: string[] | undefined,
|
||||
topicId: string,
|
||||
parentSpanId?: string,
|
||||
modelName?: string
|
||||
): Promise<KnowledgeReference[]> => {
|
||||
if (
|
||||
!extractResults.knowledge?.question ||
|
||||
@ -167,10 +214,27 @@ export const processKnowledgeSearch = async (
|
||||
return []
|
||||
}
|
||||
|
||||
const span = addSpan({
|
||||
topicId,
|
||||
name: 'knowledgeSearch',
|
||||
inputs: {
|
||||
questions,
|
||||
rewrite,
|
||||
knowledgeBaseIds: knowledgeBaseIds
|
||||
},
|
||||
tag: 'Knowledge',
|
||||
parentSpanId,
|
||||
modelName
|
||||
})
|
||||
|
||||
// 为每个知识库执行多问题搜索
|
||||
const baseSearchPromises = bases.map(async (base) => {
|
||||
// 为每个问题搜索并合并结果
|
||||
const allResults = await Promise.all(questions.map((question) => searchKnowledgeBase(question, base, rewrite)))
|
||||
const allResults = await Promise.all(
|
||||
questions.map((question) =>
|
||||
searchKnowledgeBase(question, base, rewrite, topicId, span?.spanContext().spanId, modelName)
|
||||
)
|
||||
)
|
||||
|
||||
// 合并结果并去重
|
||||
const flatResults = allResults.flat()
|
||||
@ -179,7 +243,7 @@ export const processKnowledgeSearch = async (
|
||||
).sort((a, b) => b.score - a.score)
|
||||
|
||||
// 转换为引用格式
|
||||
return await Promise.all(
|
||||
const result = await Promise.all(
|
||||
uniqueResults.map(
|
||||
async (item, index) =>
|
||||
({
|
||||
@ -190,12 +254,20 @@ export const processKnowledgeSearch = async (
|
||||
}) as KnowledgeReference
|
||||
)
|
||||
)
|
||||
return result
|
||||
})
|
||||
|
||||
// 汇总所有知识库的结果
|
||||
const resultsPerBase = await Promise.all(baseSearchPromises)
|
||||
const allReferencesRaw = resultsPerBase.flat().filter((ref): ref is KnowledgeReference => !!ref)
|
||||
|
||||
endSpan({
|
||||
topicId,
|
||||
outputs: resultsPerBase,
|
||||
span,
|
||||
modelName
|
||||
})
|
||||
|
||||
// 重新为引用分配ID
|
||||
return allReferencesRaw.map((ref, index) => ({
|
||||
...ref,
|
||||
|
||||
358
src/renderer/src/services/SpanManagerService.ts
Normal file
358
src/renderer/src/services/SpanManagerService.ts
Normal file
@ -0,0 +1,358 @@
|
||||
import { MessageStream } from '@anthropic-ai/sdk/resources/messages/messages'
|
||||
import { defaultConfig, SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { cleanContext, endContext, getContext, startContext } from '@mcp-trace/trace-web'
|
||||
import { Context, context, Span, SpanStatusCode, trace } from '@opentelemetry/api'
|
||||
import { isAsyncIterable } from '@renderer/aiCore/middleware/utils'
|
||||
import { db } from '@renderer/databases'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { handleAsyncIterable } from '@renderer/trace/dataHandler/AsyncIterableHandler'
|
||||
import { handleResult } from '@renderer/trace/dataHandler/CommonResultHandler'
|
||||
import { handleMessageStream } from '@renderer/trace/dataHandler/MessageStreamHandler'
|
||||
import { handleStream } from '@renderer/trace/dataHandler/StreamHandler'
|
||||
import { EndSpanParams, ModelSpanEntity, StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||
import { Model, Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { SdkRawChunk } from '@renderer/types/sdk'
|
||||
import { Stream } from 'openai/streaming'
|
||||
|
||||
class SpanManagerService {
|
||||
private spanMap: Map<string, ModelSpanEntity[]> = new Map()
|
||||
|
||||
getModelSpanEntity(topicId: string, modelName?: string) {
|
||||
const entities = this.spanMap.get(topicId)
|
||||
if (!entities) {
|
||||
const entity = new ModelSpanEntity(modelName)
|
||||
this.spanMap.set(topicId, [entity])
|
||||
return entity
|
||||
}
|
||||
let entity = entities.find((e) => e.getModelName() === modelName)
|
||||
if (!entity) {
|
||||
entity = new ModelSpanEntity(modelName)
|
||||
entities.push(entity)
|
||||
}
|
||||
return entity
|
||||
}
|
||||
|
||||
startTrace(params: StartSpanParams, models?: Model[]) {
|
||||
if (!defaultConfig.isDevModel) {
|
||||
console.warn('Trace is enabled in developer mode.')
|
||||
return
|
||||
}
|
||||
const span = webTracer.startSpan(params.name || 'root', {
|
||||
root: true,
|
||||
attributes: {
|
||||
inputs: JSON.stringify(params.inputs || {}),
|
||||
models: JSON.stringify(models || [])
|
||||
}
|
||||
})
|
||||
|
||||
const entity = this.getModelSpanEntity(params.topicId)
|
||||
entity.addSpan(span)
|
||||
const traceId = span.spanContext().traceId
|
||||
window.api.trace.bindTopic(params.topicId, traceId)
|
||||
|
||||
const ctx = this._updateContext(span, params.topicId)
|
||||
models?.forEach((model) => {
|
||||
this._addModelRootSpan({ ...params, name: `${model.name}.handleMessage`, modelName: model.name }, ctx)
|
||||
})
|
||||
return span
|
||||
}
|
||||
|
||||
async restartTrace(message: Message, text?: string) {
|
||||
if (!defaultConfig.isDevModel) {
|
||||
console.warn('Trace is enabled in developer mode.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!message.traceId) {
|
||||
return
|
||||
}
|
||||
|
||||
await window.api.trace.bindTopic(message.topicId, message.traceId)
|
||||
|
||||
const input = await this._getContentFromMessage(message, text)
|
||||
|
||||
let _models
|
||||
if (message.role === 'user') {
|
||||
await window.api.trace.cleanHistory(message.topicId, message.traceId)
|
||||
|
||||
const topic = await db.topics.get(message.topicId)
|
||||
_models = topic?.messages.filter((m) => m.role === 'assistant' && m.askId === message.id).map((m) => m.model)
|
||||
} else {
|
||||
_models = [message.model]
|
||||
await window.api.trace.cleanHistory(message.topicId, message.traceId || '', message.model?.name)
|
||||
}
|
||||
|
||||
_models
|
||||
?.filter((m) => !!m)
|
||||
.forEach((model) => {
|
||||
this._addModelRootSpan({ ...input, modelName: model.name, name: `${model.name}.resendMessage` })
|
||||
})
|
||||
|
||||
const modelName = message.role !== 'user' ? _models[0]?.name : undefined
|
||||
window.api.trace.openWindow(message.topicId, message.traceId, false, modelName)
|
||||
}
|
||||
|
||||
async appendTrace(message: Message, model: Model) {
|
||||
if (!defaultConfig.isDevModel) {
|
||||
console.warn('Trace is enabled in developer mode.')
|
||||
return
|
||||
}
|
||||
if (!message.traceId) {
|
||||
return
|
||||
}
|
||||
|
||||
await window.api.trace.cleanHistory(message.topicId, message.traceId, model.name)
|
||||
|
||||
const input = await this._getContentFromMessage(message)
|
||||
await window.api.trace.bindTopic(message.topicId, message.traceId)
|
||||
this._addModelRootSpan({ ...input, name: `${model.name}.appendMessage`, modelName: model.name })
|
||||
window.api.trace.openWindow(message.topicId, message.traceId, false, model.name)
|
||||
}
|
||||
|
||||
private async _getContentFromMessage(message: Message, content?: string): Promise<StartSpanParams> {
|
||||
let _content = content
|
||||
if (!_content) {
|
||||
const blocks = await Promise.all(
|
||||
message.blocks.map(async (blockId) => {
|
||||
return await db.message_blocks.get(blockId)
|
||||
})
|
||||
)
|
||||
_content = blocks.find((data) => data?.type === MessageBlockType.MAIN_TEXT)?.content
|
||||
}
|
||||
return {
|
||||
topicId: message.topicId,
|
||||
inputs: {
|
||||
messageId: message.id,
|
||||
content: _content,
|
||||
askId: message.askId,
|
||||
traceId: message.traceId,
|
||||
tag: 'resendMessage'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateContext(span: Span, topicId: string, traceId?: string) {
|
||||
window.api.trace.saveEntity({
|
||||
id: span.spanContext().spanId,
|
||||
traceId: traceId ? traceId : span.spanContext().traceId,
|
||||
topicId
|
||||
} as SpanEntity)
|
||||
if (traceId) {
|
||||
span['_spanContext'].traceId = traceId
|
||||
}
|
||||
|
||||
const ctx = trace.setSpan(context.active(), span)
|
||||
startContext(topicId, ctx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
private _addModelRootSpan(params: StartSpanParams, ctx?: Context) {
|
||||
const entity = this.getModelSpanEntity(params.topicId, params.modelName)
|
||||
const rootSpan = webTracer.startSpan(
|
||||
`${params.name}`,
|
||||
{
|
||||
attributes: {
|
||||
inputs: JSON.stringify(params.inputs || {}),
|
||||
modelName: params.modelName,
|
||||
tags: 'ModelHandle'
|
||||
}
|
||||
},
|
||||
ctx
|
||||
)
|
||||
entity.addSpan(rootSpan, true)
|
||||
const traceId = params.inputs?.traceId || rootSpan.spanContext().traceId
|
||||
return this._updateContext(rootSpan, params.topicId, traceId)
|
||||
}
|
||||
|
||||
endTrace(params: EndSpanParams) {
|
||||
const entity = this.getModelSpanEntity(params.topicId)
|
||||
let span = entity.getCurrentSpan()
|
||||
const code = params.error ? SpanStatusCode.ERROR : SpanStatusCode.OK
|
||||
const message = params.error ? params.error.message : ''
|
||||
while (span) {
|
||||
if (params.outputs) {
|
||||
span.setAttributes({ outputs: params.outputs })
|
||||
}
|
||||
if (params.error) {
|
||||
span.recordException(params.error)
|
||||
}
|
||||
span.setStatus({ code, message })
|
||||
span.end()
|
||||
entity.removeSpan(span)
|
||||
span = entity.getCurrentSpan()
|
||||
}
|
||||
this.finishModelTrace(params.topicId)
|
||||
cleanContext(params.topicId)
|
||||
window.api.trace.saveData(params.topicId)
|
||||
}
|
||||
|
||||
addSpan(params: StartSpanParams) {
|
||||
if (!defaultConfig.isDevModel) {
|
||||
console.warn('Trace is enabled in developer mode.')
|
||||
return
|
||||
}
|
||||
const entity = this.getModelSpanEntity(params.topicId, params.modelName)
|
||||
let parentSpan = entity.getSpanById(params.parentSpanId)
|
||||
if (!parentSpan) {
|
||||
parentSpan = this.getCurrentSpan(params.topicId, params.modelName)
|
||||
}
|
||||
|
||||
const parentCtx = parentSpan ? trace.setSpan(context.active(), parentSpan) : getContext(params.topicId)
|
||||
const span = webTracer.startSpan(
|
||||
params.name || 'root',
|
||||
{
|
||||
attributes: {
|
||||
inputs: JSON.stringify(params.inputs || {}),
|
||||
tags: params.tag || '',
|
||||
modelName: params.modelName
|
||||
}
|
||||
},
|
||||
parentCtx
|
||||
)
|
||||
const ctx = trace.setSpan(getContext(params.topicId), span)
|
||||
entity.addSpan(span)
|
||||
startContext(params.topicId, ctx)
|
||||
return span
|
||||
}
|
||||
|
||||
endSpan(params: EndSpanParams) {
|
||||
const entity = this.getModelSpanEntity(params.topicId, params.modelName)
|
||||
const span = params.span || entity.getCurrentSpan(params.modelName)
|
||||
if (params.modelEnded && params.modelName && params.outputs) {
|
||||
const rootEntity = this.getModelSpanEntity(params.topicId)
|
||||
const span = rootEntity?.getRootSpan()
|
||||
window.api.trace.addEndMessage(span?.spanContext().spanId || '', params.modelName, params.outputs)
|
||||
}
|
||||
if (params.modelEnded && params.error && params.modelName) {
|
||||
const rootEntity = this.getModelSpanEntity(params.topicId)
|
||||
rootEntity.addModelError(params.error)
|
||||
}
|
||||
if (!span) {
|
||||
console.info(`No active span found for topicId: ${params.topicId}-modelName: ${params.modelName}.`)
|
||||
return
|
||||
}
|
||||
|
||||
// remove span
|
||||
if (entity.removeSpan(span)) {
|
||||
this.getModelSpanEntity(params.topicId).removeSpan(span)
|
||||
}
|
||||
|
||||
const code = params.error ? SpanStatusCode.ERROR : SpanStatusCode.OK
|
||||
const message = params.error ? params.error.message : 'success'
|
||||
if (params.outputs) {
|
||||
span.setAttributes({ outputs: JSON.stringify(params.outputs || {}) })
|
||||
}
|
||||
if (params.error) {
|
||||
span.recordException(params.error)
|
||||
}
|
||||
span.setStatus({ code, message })
|
||||
span.end()
|
||||
endContext(params.topicId)
|
||||
}
|
||||
|
||||
getCurrentSpan(topicId: string, modelName?: string, isRoot = false): Span | undefined {
|
||||
let entity = this.getModelSpanEntity(topicId, modelName)
|
||||
let span = isRoot ? entity.getRoot() : entity.getCurrentSpan(modelName)
|
||||
if (!span && modelName) {
|
||||
entity = this.getModelSpanEntity(topicId)
|
||||
span = entity.getCurrentSpan()
|
||||
}
|
||||
return span
|
||||
}
|
||||
|
||||
async addTokenUsage(topicId: string, prompt: number, completion: number) {
|
||||
const span = this.getCurrentSpan(topicId)
|
||||
const usage: TokenUsage = {
|
||||
prompt_tokens: prompt,
|
||||
completion_tokens: completion,
|
||||
total_tokens: prompt + completion
|
||||
}
|
||||
if (span) {
|
||||
window.api.trace.tokenUsage(span.spanContext().spanId, usage)
|
||||
}
|
||||
}
|
||||
|
||||
async finishModelTrace(topicId: string) {
|
||||
this.spanMap.get(topicId)?.forEach((entity) => entity.finishSpan())
|
||||
this.spanMap.delete(topicId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a function and executes it within a span, returning the function's result instead of the wrapped function.
|
||||
* @param fn The function to execute.
|
||||
* @param name The span name.
|
||||
* @param tags The span tags.
|
||||
* @param getTopicId Function to get topicId from arguments.
|
||||
* @returns The result of the executed function.
|
||||
*/
|
||||
export function withSpanResult<F extends (...args: any) => any>(
|
||||
fn: F,
|
||||
params: StartSpanParams,
|
||||
...args: Parameters<F>
|
||||
): ReturnType<F> {
|
||||
if (!params.topicId || params.topicId === '') {
|
||||
return fn(...args)
|
||||
}
|
||||
const span = addSpan({
|
||||
topicId: params.topicId,
|
||||
name: params.name,
|
||||
tag: params.tag,
|
||||
inputs: args,
|
||||
parentSpanId: params.parentSpanId,
|
||||
modelName: params.modelName
|
||||
})
|
||||
try {
|
||||
const result = fn(...args)
|
||||
if (result instanceof Promise) {
|
||||
return result
|
||||
.then((data) => {
|
||||
if (!data || typeof data !== 'object') {
|
||||
endSpan({ topicId: params.topicId, outputs: data, span, modelName: params.modelName })
|
||||
return data
|
||||
}
|
||||
|
||||
if (data instanceof Stream) {
|
||||
return handleStream(data, span, params.topicId, params.modelName)
|
||||
} else if (data instanceof MessageStream) {
|
||||
return handleMessageStream(data, span, params.topicId, params.modelName)
|
||||
} else if (isAsyncIterable<SdkRawChunk>(data)) {
|
||||
return handleAsyncIterable(data, span, params.topicId, params.modelName)
|
||||
} else {
|
||||
return handleResult(data, span, params.topicId, params.modelName)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
endSpan({ topicId: params.topicId, error: err, span, modelName: params.modelName })
|
||||
throw err
|
||||
}) as ReturnType<F>
|
||||
} else {
|
||||
endSpan({ topicId: params.topicId, outputs: result, span, modelName: params.modelName })
|
||||
return result
|
||||
}
|
||||
} catch (err) {
|
||||
endSpan({ topicId: params.topicId, error: err as Error, span, modelName: params.modelName })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export const spanManagerService = new SpanManagerService()
|
||||
export const webTracer = trace.getTracer('CherryStudio', '1.0.0')
|
||||
export const addSpan = spanManagerService.addSpan.bind(spanManagerService)
|
||||
export const startTrace = spanManagerService.startTrace.bind(spanManagerService)
|
||||
export const endTrace = spanManagerService.endTrace.bind(spanManagerService)
|
||||
export const endSpan = spanManagerService.endSpan.bind(spanManagerService)
|
||||
export const currentSpan = spanManagerService.getCurrentSpan.bind(spanManagerService)
|
||||
export const addTokenUsage = spanManagerService.addTokenUsage.bind(spanManagerService)
|
||||
export const pauseTrace = spanManagerService.finishModelTrace.bind(spanManagerService)
|
||||
export const appendTrace = spanManagerService.appendTrace.bind(spanManagerService)
|
||||
export const restartTrace = spanManagerService.restartTrace.bind(spanManagerService)
|
||||
|
||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, ({ topicId, traceId }) => {
|
||||
window.api.trace.openWindow(topicId, traceId, false)
|
||||
})
|
||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, (topic: Topic) => {
|
||||
window.api.trace.cleanTopic(topic.id)
|
||||
})
|
||||
@ -2,6 +2,7 @@ import { loggerService } from '@logger'
|
||||
import { DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT } from '@renderer/config/constant'
|
||||
import i18n from '@renderer/i18n'
|
||||
import WebSearchEngineProvider from '@renderer/providers/WebSearchProvider'
|
||||
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import store from '@renderer/store'
|
||||
import { setWebSearchStatus } from '@renderer/store/runtime'
|
||||
import { CompressionConfig, WebSearchState } from '@renderer/store/websearch'
|
||||
@ -164,10 +165,11 @@ class WebSearchService {
|
||||
public async search(
|
||||
provider: WebSearchProvider,
|
||||
query: string,
|
||||
httpOptions?: RequestInit
|
||||
httpOptions?: RequestInit,
|
||||
spanId?: string
|
||||
): Promise<WebSearchProviderResponse> {
|
||||
const websearch = this.getWebSearchState()
|
||||
const webSearchEngine = new WebSearchEngineProvider(provider)
|
||||
const webSearchEngine = new WebSearchEngineProvider(provider, spanId)
|
||||
|
||||
let formattedQuery = query
|
||||
// FIXME: 有待商榷,效果一般
|
||||
@ -440,16 +442,38 @@ class WebSearchService {
|
||||
// 使用请求特定的signal,如果没有则回退到全局signal
|
||||
const signal = this.getRequestState(requestId).signal || this.signal
|
||||
|
||||
const span = webSearchProvider.topicId
|
||||
? addSpan({
|
||||
topicId: webSearchProvider.topicId,
|
||||
name: `WebSearch`,
|
||||
inputs: {
|
||||
question: extractResults.websearch.question,
|
||||
provider: webSearchProvider.id
|
||||
},
|
||||
tag: `Web`,
|
||||
parentSpanId: webSearchProvider.parentSpanId,
|
||||
modelName: webSearchProvider.modelName
|
||||
})
|
||||
: undefined
|
||||
const questions = extractResults.websearch.question
|
||||
const links = extractResults.websearch.links
|
||||
|
||||
// 处理 summarize
|
||||
if (questions[0] === 'summarize' && links && links.length > 0) {
|
||||
const contents = await fetchWebContents(links, undefined, undefined, { signal })
|
||||
webSearchProvider.topicId &&
|
||||
endSpan({
|
||||
topicId: webSearchProvider.topicId,
|
||||
outputs: contents,
|
||||
modelName: webSearchProvider.modelName,
|
||||
span
|
||||
})
|
||||
return { query: 'summaries', results: contents }
|
||||
}
|
||||
|
||||
const searchPromises = questions.map((q) => this.search(webSearchProvider, q, { signal }))
|
||||
const searchPromises = questions.map((q) =>
|
||||
this.search(webSearchProvider, q, { signal }, span?.spanContext().spanId)
|
||||
)
|
||||
const searchResults = await Promise.allSettled(searchPromises)
|
||||
|
||||
// 统计成功完成的搜索数量
|
||||
@ -480,6 +504,14 @@ class WebSearchService {
|
||||
// 如果没有搜索结果,直接返回空结果
|
||||
if (finalResults.length === 0) {
|
||||
await this.setWebSearchStatus(requestId, { phase: 'default' })
|
||||
if (webSearchProvider.topicId) {
|
||||
endSpan({
|
||||
topicId: webSearchProvider.topicId,
|
||||
outputs: finalResults,
|
||||
modelName: webSearchProvider.modelName,
|
||||
span
|
||||
})
|
||||
}
|
||||
return {
|
||||
query: questions.join(' | '),
|
||||
results: []
|
||||
@ -526,6 +558,14 @@ class WebSearchService {
|
||||
// 重置状态
|
||||
await this.setWebSearchStatus(requestId, { phase: 'default' })
|
||||
|
||||
if (webSearchProvider.topicId) {
|
||||
endSpan({
|
||||
topicId: webSearchProvider.topicId,
|
||||
outputs: finalResults,
|
||||
modelName: webSearchProvider.modelName,
|
||||
span
|
||||
})
|
||||
}
|
||||
return {
|
||||
query: questions.join(' | '),
|
||||
results: finalResults
|
||||
|
||||
34
src/renderer/src/services/WebTraceService.ts
Normal file
34
src/renderer/src/services/WebTraceService.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { convertSpanToSpanEntity, FunctionSpanExporter, FunctionSpanProcessor } from '@mcp-trace/trace-core'
|
||||
import { WebTracer } from '@mcp-trace/trace-web'
|
||||
import { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||
|
||||
const TRACER_NAME = 'CherryStudio'
|
||||
|
||||
class WebTraceService {
|
||||
init() {
|
||||
const exporter = new FunctionSpanExporter((spans: ReadableSpan[]): Promise<void> => {
|
||||
// Implement your save logic here if needed
|
||||
// For now, just resolve immediately
|
||||
console.log('Saving spans:', spans)
|
||||
return Promise.resolve()
|
||||
})
|
||||
const processor = new FunctionSpanProcessor(
|
||||
exporter,
|
||||
(span: ReadableSpan) => {
|
||||
window.api.trace.saveEntity(convertSpanToSpanEntity(span))
|
||||
},
|
||||
(span: ReadableSpan) => {
|
||||
window.api.trace.saveEntity(convertSpanToSpanEntity(span))
|
||||
}
|
||||
)
|
||||
WebTracer.init(
|
||||
{
|
||||
defaultTracerName: TRACER_NAME,
|
||||
serviceName: TRACER_NAME
|
||||
},
|
||||
processor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const webTraceService = new WebTraceService()
|
||||
@ -4,6 +4,7 @@ import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { BlockManager } from '@renderer/services/messageStreaming/BlockManager'
|
||||
import { createCallbacks } from '@renderer/services/messageStreaming/callbacks'
|
||||
import { endSpan } from '@renderer/services/SpanManagerService'
|
||||
import { createStreamProcessor, type StreamProcessorCallbacks } from '@renderer/services/StreamProcessingService'
|
||||
import store from '@renderer/store'
|
||||
import { updateTopicUpdatedAt } from '@renderer/store/assistants'
|
||||
@ -258,7 +259,8 @@ const dispatchMultiModelResponses = async (
|
||||
const assistantMessage = createAssistantMessage(assistant.id, topicId, {
|
||||
askId: triggeringMessage.id,
|
||||
model: mentionedModel,
|
||||
modelId: mentionedModel.id
|
||||
modelId: mentionedModel.id,
|
||||
traceId: triggeringMessage.traceId
|
||||
})
|
||||
dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
|
||||
assistantMessageStubs.push(assistantMessage)
|
||||
@ -886,13 +888,24 @@ const fetchAndProcessAssistantResponseImpl = async (
|
||||
const streamProcessorCallbacks = createStreamProcessor(callbacks)
|
||||
|
||||
// const startTime = Date.now()
|
||||
await fetchChatCompletion({
|
||||
const result = await fetchChatCompletion({
|
||||
messages: messagesForContext,
|
||||
assistant: assistant,
|
||||
onChunkReceived: streamProcessorCallbacks
|
||||
})
|
||||
endSpan({
|
||||
topicId,
|
||||
outputs: result ? result.getText() : '',
|
||||
modelName: assistant.model?.name,
|
||||
modelEnded: true
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching chat completion:', error)
|
||||
endSpan({
|
||||
topicId,
|
||||
error: error,
|
||||
modelName: assistant.model?.name
|
||||
})
|
||||
if (assistantMessage) {
|
||||
callbacks.onError?.(error)
|
||||
throw error
|
||||
@ -930,7 +943,8 @@ export const sendMessage =
|
||||
} else {
|
||||
const assistantMessage = createAssistantMessage(assistant.id, topicId, {
|
||||
askId: userMessage.id,
|
||||
model: assistant.model
|
||||
model: assistant.model,
|
||||
traceId: userMessage.traceId
|
||||
})
|
||||
await saveMessageAndBlocksToDB(assistantMessage, [])
|
||||
dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
|
||||
@ -1129,6 +1143,7 @@ export const resendMessageThunk =
|
||||
askId: userMessageToResend.id,
|
||||
model: assistant.model
|
||||
})
|
||||
assistantMessage.traceId = userMessageToResend.traceId
|
||||
resetDataList.push(assistantMessage)
|
||||
|
||||
resetDataList.forEach((message) => {
|
||||
@ -1427,7 +1442,8 @@ export const appendAssistantResponseThunk =
|
||||
topicId: Topic['id'],
|
||||
existingAssistantMessageId: string, // ID of the assistant message the user interacted with
|
||||
newModel: Model, // The new model selected by the user
|
||||
assistant: Assistant // Base assistant configuration
|
||||
assistant: Assistant, // Base assistant configuration
|
||||
traceId?: string
|
||||
) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
try {
|
||||
@ -1474,7 +1490,8 @@ export const appendAssistantResponseThunk =
|
||||
const newAssistantStub = createAssistantMessage(assistant.id, topicId, {
|
||||
askId: askId, // Crucial: Use the original askId
|
||||
model: newModel,
|
||||
modelId: newModel.id
|
||||
modelId: newModel.id,
|
||||
traceId: traceId
|
||||
})
|
||||
|
||||
// 3. Update Redux Store
|
||||
|
||||
98
src/renderer/src/trace/dataHandler/AsyncIterableHandler.ts
Normal file
98
src/renderer/src/trace/dataHandler/AsyncIterableHandler.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { Span } from '@opentelemetry/api'
|
||||
import { endSpan } from '@renderer/services/SpanManagerService'
|
||||
import { SdkRawChunk } from '@renderer/types/sdk'
|
||||
|
||||
export class AsyncIterableHandler {
|
||||
private span: Span
|
||||
private stream: AsyncIterable<SdkRawChunk>
|
||||
private topicId: string
|
||||
private usageToken: TokenUsage
|
||||
private modelName?: string
|
||||
constructor(stream: AsyncIterable<SdkRawChunk>, span: Span, topicId: string, modelName?: string) {
|
||||
this.stream = this.transformStream(stream)
|
||||
this.span = span
|
||||
this.topicId = topicId
|
||||
this.modelName = modelName
|
||||
this.usageToken = {
|
||||
completion_tokens: 0,
|
||||
prompt_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
}
|
||||
|
||||
async handleChunk(chunk: SdkRawChunk) {
|
||||
let context = 'choices' in chunk ? chunk.choices.map((ch) => ch.delta.context).join() : ''
|
||||
if (!context && 'candidates' in chunk && chunk.candidates) {
|
||||
context = chunk.candidates
|
||||
.map(
|
||||
(ch) =>
|
||||
ch.content?.parts
|
||||
?.map((p) => {
|
||||
if (p.text) {
|
||||
return p.text
|
||||
} else if (p.functionCall) {
|
||||
return `${p.functionCall.name}(${JSON.stringify(p.functionCall.args || '')})`
|
||||
} else if (p.codeExecutionResult) {
|
||||
return p.codeExecutionResult.output || String(p.codeExecutionResult.outcome || '')
|
||||
} else if (p.executableCode) {
|
||||
return `'''${p.executableCode.language || ''}\n${p.executableCode.code}\n'''`
|
||||
} else if (p.fileData) {
|
||||
return '<Blob Data>'
|
||||
} else if (p.functionResponse) {
|
||||
return `${p.functionResponse.name}: ${JSON.stringify(p.functionResponse.response)}`
|
||||
} else if (p.inlineData) {
|
||||
return '<File Data>'
|
||||
} else if (p.videoMetadata) {
|
||||
return `fps: ${p.videoMetadata.fps}, start:${p.videoMetadata.startOffset}, end:${p.videoMetadata.endOffset}`
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
.join() || ''
|
||||
)
|
||||
.join()
|
||||
}
|
||||
if (context) {
|
||||
window.api.trace.addStreamMessage(this.span.spanContext().spanId, this.modelName || '', context, chunk)
|
||||
}
|
||||
if ('usageMetadata' in chunk && chunk.usageMetadata) {
|
||||
this.usageToken.prompt_tokens = chunk.usageMetadata.promptTokenCount || 0
|
||||
this.usageToken.total_tokens = chunk.usageMetadata.totalTokenCount || 0
|
||||
this.usageToken.completion_tokens =
|
||||
(chunk.usageMetadata.totalTokenCount || 0) - (chunk.usageMetadata.promptTokenCount || 0)
|
||||
}
|
||||
}
|
||||
|
||||
async finish() {
|
||||
window.api.trace.tokenUsage(this.span.spanContext().spanId, this.usageToken)
|
||||
endSpan({ topicId: this.topicId, span: this.span, modelName: this.modelName })
|
||||
}
|
||||
|
||||
async handleError(err) {
|
||||
endSpan({ topicId: this.topicId, error: err, span: this.span, modelName: this.modelName })
|
||||
}
|
||||
|
||||
async *transformStream(stream: AsyncIterable<SdkRawChunk>) {
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
this.handleChunk(chunk)
|
||||
yield chunk
|
||||
}
|
||||
} catch (err) {
|
||||
this.handleError(err)
|
||||
throw err
|
||||
}
|
||||
this.finish()
|
||||
}
|
||||
|
||||
static handleStream(stream: AsyncIterable<SdkRawChunk>, span?: Span, topicId?: string, modelName?: string) {
|
||||
if (!span || !topicId) {
|
||||
return stream
|
||||
}
|
||||
const handler = new AsyncIterableHandler(stream, span!, topicId, modelName)
|
||||
return handler.stream
|
||||
}
|
||||
}
|
||||
|
||||
export const handleAsyncIterable = AsyncIterableHandler.handleStream
|
||||
77
src/renderer/src/trace/dataHandler/CommonResultHandler.ts
Normal file
77
src/renderer/src/trace/dataHandler/CommonResultHandler.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { Span } from '@opentelemetry/api'
|
||||
import { CompletionsResult } from '@renderer/aiCore/middleware/schemas'
|
||||
import { endSpan } from '@renderer/services/SpanManagerService'
|
||||
|
||||
export class CompletionsResultHandler {
|
||||
private data: any
|
||||
private tokenUsage?: TokenUsage
|
||||
private span: Span
|
||||
private topicId: string
|
||||
private modelName?: string
|
||||
|
||||
constructor(data: any, span: Span, topicId: string, modelName?: string) {
|
||||
this.data = data && this.isCompletionsResult(data) ? { ...data, finishText: data.getText() } : data
|
||||
this.span = span
|
||||
this.topicId = topicId
|
||||
this.tokenUsage = this.getUsage(data)
|
||||
this.modelName = modelName
|
||||
}
|
||||
|
||||
isCompletionsResult(data: any): data is CompletionsResult {
|
||||
return (
|
||||
data !== null &&
|
||||
typeof data === 'object' &&
|
||||
typeof data.getText === 'function' &&
|
||||
(data.rawOutput === undefined || typeof data.rawOutput === 'object') &&
|
||||
(data.stream === undefined || typeof data.stream === 'object') &&
|
||||
(data.controller === undefined || data.controller instanceof AbortController)
|
||||
)
|
||||
}
|
||||
|
||||
getUsage(data?: any): TokenUsage | undefined {
|
||||
// Replace this with an appropriate property check for CompletionsResult
|
||||
if (!data || typeof data !== 'object' || !('usage' in data || 'usageMetadata' in data)) {
|
||||
return undefined
|
||||
}
|
||||
const tokens: TokenUsage = {
|
||||
completion_tokens: 0,
|
||||
prompt_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
if ('usage' in data) {
|
||||
const usage = data.usage
|
||||
tokens.completion_tokens = usage['completion_tokens'] || 0
|
||||
tokens.prompt_tokens = usage['prompt_tokens'] || 0
|
||||
tokens.total_tokens = usage['total_tokens'] || 0
|
||||
// Do something with usage
|
||||
} else {
|
||||
const usage = data.usageMetadata
|
||||
tokens.completion_tokens = usage['thoughtsTokenCount'] || 0
|
||||
tokens.prompt_tokens = usage['promptTokenCount'] || 0
|
||||
tokens.total_tokens = usage['totalTokenCount'] || 0
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
finish() {
|
||||
if (this.tokenUsage) {
|
||||
window.api.trace.tokenUsage(this.span.spanContext().spanId, this.tokenUsage)
|
||||
}
|
||||
if (this.data) {
|
||||
endSpan({ topicId: this.topicId, outputs: this.data, span: this.span, modelName: this.modelName })
|
||||
} else {
|
||||
endSpan({ topicId: this.topicId, span: this.span, modelName: this.modelName })
|
||||
}
|
||||
}
|
||||
|
||||
static handleResult(data?: any, span?: Span, topicId?: string, modelName?: string) {
|
||||
if (span && topicId) {
|
||||
const handler = new CompletionsResultHandler(data, span!, topicId, modelName)
|
||||
handler.finish()
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
export const handleResult = CompletionsResultHandler.handleResult
|
||||
70
src/renderer/src/trace/dataHandler/MessageStreamHandler.ts
Normal file
70
src/renderer/src/trace/dataHandler/MessageStreamHandler.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { Message, MessageStream } from '@anthropic-ai/sdk/resources/messages/messages'
|
||||
import { TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { Span } from '@opentelemetry/api'
|
||||
import { endSpan } from '@renderer/services/SpanManagerService'
|
||||
|
||||
export class MessageStreamHandler {
|
||||
private span: Span
|
||||
private stream: MessageStream
|
||||
private topicId: string
|
||||
private tokenUsage: TokenUsage
|
||||
private modelName?: string
|
||||
|
||||
constructor(stream: MessageStream, span: Span, topicId: string, modelName?: string) {
|
||||
this.stream = stream
|
||||
this.span = span
|
||||
this.topicId = topicId
|
||||
this.tokenUsage = {
|
||||
completion_tokens: 0,
|
||||
prompt_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
stream.on('error', (err) => {
|
||||
endSpan({ topicId, error: err, span, modelName: this.modelName })
|
||||
})
|
||||
stream.on('message', (message) => this.write(message))
|
||||
stream.on('end', () => this.finish())
|
||||
this.modelName = modelName
|
||||
}
|
||||
|
||||
async finish() {
|
||||
window.api.trace.tokenUsage(this.span.spanContext().spanId, this.tokenUsage)
|
||||
endSpan({ topicId: this.topicId, span: this.span, modelName: this.modelName })
|
||||
}
|
||||
|
||||
async write(message: Message) {
|
||||
if (message.usage) {
|
||||
this.tokenUsage.completion_tokens += message.usage.output_tokens
|
||||
this.tokenUsage.prompt_tokens += message.usage.input_tokens
|
||||
this.tokenUsage.total_tokens += message.usage.output_tokens + message.usage.input_tokens
|
||||
}
|
||||
const context = message.content
|
||||
.map((c) => {
|
||||
if (c.type === 'text') {
|
||||
return c.text
|
||||
} else if (c.type === 'redacted_thinking') {
|
||||
return c.data
|
||||
} else if (c.type === 'server_tool_use' || c.type === 'tool_use') {
|
||||
return `${c.name}: ${c.input}`
|
||||
} else if (c.type === 'thinking') {
|
||||
return c.thinking
|
||||
} else if (c.type === 'web_search_tool_result') {
|
||||
return c.content
|
||||
} else {
|
||||
return JSON.stringify(c)
|
||||
}
|
||||
})
|
||||
.join()
|
||||
window.api.trace.addStreamMessage(this.span.spanContext().spanId, this.modelName || '', context, message)
|
||||
}
|
||||
|
||||
static handleStream(stream: MessageStream, span?: Span, topicId?: string, modelName?: string) {
|
||||
if (!span || !topicId) {
|
||||
return stream
|
||||
}
|
||||
const handler = new MessageStreamHandler(stream, span!, topicId, modelName)
|
||||
return handler.stream
|
||||
}
|
||||
}
|
||||
|
||||
export const handleMessageStream = MessageStreamHandler.handleStream
|
||||
110
src/renderer/src/trace/dataHandler/StreamHandler.ts
Normal file
110
src/renderer/src/trace/dataHandler/StreamHandler.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { Span } from '@opentelemetry/api'
|
||||
import { endSpan } from '@renderer/services/SpanManagerService'
|
||||
import { OpenAI } from 'openai'
|
||||
import { Stream } from 'openai/streaming'
|
||||
|
||||
export class StreamHandler {
|
||||
private topicId: string
|
||||
private span: Span
|
||||
private modelName?: string
|
||||
private usage: TokenUsage = {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0
|
||||
}
|
||||
private stream: Stream<OpenAI.Chat.Completions.ChatCompletionChunk | OpenAI.Responses.ResponseStreamEvent>
|
||||
|
||||
constructor(
|
||||
topicId: string,
|
||||
span: Span,
|
||||
stream: Stream<OpenAI.Chat.Completions.ChatCompletionChunk | OpenAI.Responses.ResponseStreamEvent>,
|
||||
modelName?: string
|
||||
) {
|
||||
this.topicId = topicId
|
||||
this.span = span
|
||||
this.modelName = modelName
|
||||
this.stream = stream
|
||||
}
|
||||
|
||||
async *createStreamAdapter(): AsyncIterable<
|
||||
OpenAI.Chat.Completions.ChatCompletionChunk | OpenAI.Responses.ResponseStreamEvent
|
||||
> {
|
||||
try {
|
||||
for await (const chunk of this.stream) {
|
||||
let context: string | undefined
|
||||
if ('object' in chunk && chunk.object === 'chat.completion.chunk') {
|
||||
const completionChunk = chunk as OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
if (completionChunk.usage) {
|
||||
this.usage.completion_tokens += completionChunk.usage.completion_tokens || 0
|
||||
this.usage.prompt_tokens += completionChunk.usage.prompt_tokens || 0
|
||||
this.usage.total_tokens += completionChunk.usage.total_tokens || 0
|
||||
}
|
||||
context = chunk.choices
|
||||
.map((choice) => {
|
||||
if (!choice.delta) {
|
||||
return ''
|
||||
} else if ('reasoning_content' in choice.delta) {
|
||||
return choice.delta.reasoning_content
|
||||
} else if (choice.delta.content) {
|
||||
return choice.delta.content
|
||||
} else if (choice.delta.refusal) {
|
||||
return choice.delta.refusal
|
||||
} else if (choice.delta.tool_calls) {
|
||||
return choice.delta.tool_calls.map((toolCall) => {
|
||||
return toolCall.function?.name || toolCall.function?.arguments
|
||||
})
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.join()
|
||||
} else {
|
||||
const resp = chunk as OpenAI.Responses.ResponseStreamEvent
|
||||
if ('response' in resp && resp.response) {
|
||||
context = resp.response.output_text
|
||||
if (resp.response.usage) {
|
||||
this.usage.completion_tokens += resp.response.usage.output_tokens || 0
|
||||
this.usage.prompt_tokens += resp.response.usage.input_tokens || 0
|
||||
this.usage.total_tokens += (resp.response.usage.input_tokens || 0) + resp.response.usage.output_tokens
|
||||
}
|
||||
} else if ('delta' in resp && resp.delta) {
|
||||
context = typeof resp.delta === 'string' ? resp.delta : JSON.stringify(resp.delta)
|
||||
} else if ('text' in resp && resp.text) {
|
||||
context = resp.text
|
||||
} else if ('partial_image_b64' in resp && resp.partial_image_b64) {
|
||||
context = '<Image Data>'
|
||||
} else if ('part' in resp && resp.part) {
|
||||
context = 'refusal' in resp.part ? resp.part.refusal : resp.part.text
|
||||
} else {
|
||||
context = ''
|
||||
}
|
||||
}
|
||||
window.api.trace.addStreamMessage(this.span.spanContext().spanId, this.modelName || '', context, chunk)
|
||||
yield chunk
|
||||
}
|
||||
this.finish()
|
||||
} catch (err) {
|
||||
endSpan({ topicId: this.topicId, error: err as Error, span: this.span, modelName: this.modelName })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async finish() {
|
||||
window.api.trace.tokenUsage(this.span.spanContext().spanId, this.usage)
|
||||
endSpan({ topicId: this.topicId, span: this.span, modelName: this.modelName })
|
||||
}
|
||||
|
||||
static handleStream(
|
||||
stream: Stream<OpenAI.Chat.Completions.ChatCompletionChunk | OpenAI.Responses.ResponseStreamEvent>,
|
||||
span?: Span,
|
||||
topicId?: string,
|
||||
modelName?: string
|
||||
) {
|
||||
if (!span || !topicId) {
|
||||
return stream
|
||||
}
|
||||
return new StreamHandler(topicId, span, stream, modelName).createStreamAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
export const handleStream = StreamHandler.handleStream
|
||||
164
src/renderer/src/trace/pages/Component.tsx
Normal file
164
src/renderer/src/trace/pages/Component.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
|
||||
// Box 组件
|
||||
export const Box: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement> & { padding?: number; border?: string; borderStyle?: string; className?: string }
|
||||
> = ({ padding: p, border, borderStyle, className, style, ...props }) => (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
padding: p ? `${p}px` : undefined,
|
||||
border: border,
|
||||
borderStyle: borderStyle,
|
||||
...style
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
// SimpleGrid 组件
|
||||
export const SimpleGrid: React.FC<{
|
||||
columns?: number
|
||||
templateColumns?: string
|
||||
children: React.ReactNode
|
||||
leftSpace?: number
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>
|
||||
}> = ({ columns, templateColumns, children, leftSpace = 0, style, className, onClick, ...props }) => (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: templateColumns || (columns ? `repeat(${columns}, 1fr)` : undefined),
|
||||
gap: '1px',
|
||||
paddingLeft: leftSpace,
|
||||
...style
|
||||
}}
|
||||
onClick={onClick}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Text 组件
|
||||
export const Text: React.FC<React.HTMLAttributes<HTMLSpanElement>> = ({ style, className, ...props }) => (
|
||||
<span
|
||||
style={{ fontSize: 12, ...style, cursor: props.onClick ? 'pointer' : undefined }}
|
||||
className={className}
|
||||
{...props}
|
||||
onClick={props.onClick ? props.onClick : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
// VStack 组件
|
||||
export const VStack: React.FC<{ grap?: number; align?: string; children: React.ReactNode }> = ({
|
||||
grap = 5,
|
||||
align = 'stretch',
|
||||
children,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: align,
|
||||
gap: `${grap}px`
|
||||
}}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
// GridItem 组件
|
||||
export const GridItem: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement> & { colSpan?: number; rowSpan?: number; padding?: number }
|
||||
> = ({ colSpan, rowSpan, padding, style, ...props }) => (
|
||||
<div
|
||||
style={{
|
||||
gridColumn: colSpan ? `span ${colSpan}` : undefined,
|
||||
gridRow: rowSpan ? `span ${rowSpan}` : undefined,
|
||||
padding: padding ? `${padding}px` : undefined,
|
||||
textAlign: 'center',
|
||||
...style
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
// HStack 组件
|
||||
export const HStack: React.FC<{ grap?: number; children: React.ReactNode; style?: React.CSSProperties }> = ({
|
||||
grap,
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: grap ? `${grap}px` : '5px',
|
||||
...style
|
||||
}}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
// IconButton 组件
|
||||
export const IconButton: React.FC<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement> & { size?: 'sm' | 'md'; fontSize?: string }
|
||||
> = ({ size = 'md', fontSize = '12px', style, onClick, ...props }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: size === 'sm' ? 12 : 20,
|
||||
height: 24,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...style
|
||||
}}
|
||||
{...props}>
|
||||
{props.children ||
|
||||
(props['aria-label'] === 'Toggle' ? props['aria-expanded'] ? <CaretDownOutlined /> : <CaretRightOutlined /> : '')}
|
||||
</button>
|
||||
)
|
||||
|
||||
// 自定义 Button 组件
|
||||
export const Button: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement>> = ({ style, ...props }) => (
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
padding: '5px 10px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
...style
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export const TraceIcon = ({ size = 200, color = 'currentColor', className = 'icon' }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M919.296 515.072a93.3376 93.3376 0 0 0-31.6928 5.8368l-142.6944-214.1184a108.5952 108.5952 0 0 0 17.3056-58.7264 109.9776 109.9776 0 1 0-192.256 72.192l-143.8208 263.7312a151.5008 151.5008 0 0 0-40.96-6.0928 155.8528 155.8528 0 0 0-84.6848 25.1904l-115.2-138.24a93.2352 93.2352 0 0 0 11.4176-44.032 94.2592 94.2592 0 1 0-57.6 87.04l116.0704 139.264a157.3376 157.3376 0 1 0 226.9184-34.56l141.1072-258.7136a104.0384 104.0384 0 0 0 73.728-5.12l141.7728 212.6336a94.0032 94.0032 0 1 0 80.4864-46.08zM385.28 829.44a94.2592 94.2592 0 1 1 94.208-94.2592A94.3616 94.3616 0 0 1 385.28 829.44z m0 0"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
33
src/renderer/src/trace/pages/ProgressBar.tsx
Normal file
33
src/renderer/src/trace/pages/ProgressBar.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
export interface ProgressBarProps {
|
||||
start: number
|
||||
progress: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export const ProgressBar: React.FC<ProgressBarProps> = ({ start = 0, progress, height = 6 }) => {
|
||||
const displayProgress = Math.max(0, progress)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: '#e0e0e0',
|
||||
borderRadius: height,
|
||||
overflow: 'hidden',
|
||||
marginTop: '8px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: `${displayProgress}%`,
|
||||
height: height,
|
||||
backgroundColor: '#4CAF50',
|
||||
borderRadius: height,
|
||||
transition: 'width 0.3s ease',
|
||||
marginLeft: `${start}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
171
src/renderer/src/trace/pages/SpanDetail.tsx
Normal file
171
src/renderer/src/trace/pages/SpanDetail.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import './Trace.css'
|
||||
|
||||
import { DoubleLeftOutlined } from '@ant-design/icons'
|
||||
// import TraceModal from '@renderer/trace/TraceModal'
|
||||
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactJson from 'react-json-view'
|
||||
|
||||
import { Box, Button, Text } from './Component'
|
||||
import { convertTime } from './TraceTree'
|
||||
|
||||
interface SpanDetailProps {
|
||||
node: TraceModal
|
||||
clickShowModal: (input: boolean) => void
|
||||
}
|
||||
|
||||
const SpanDetail: FC<SpanDetailProps> = ({ node, clickShowModal }) => {
|
||||
const [showInput, setShowInput] = useState(true)
|
||||
const [jsonData, setJsonData] = useState<object>({})
|
||||
const [isJson, setIsJson] = useState(false)
|
||||
const [usedTime, setUsedTime] = useState<string>('')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const changeJsonData = useCallback(() => {
|
||||
let data: any = {}
|
||||
if (!node.attributes) {
|
||||
setJsonData(data)
|
||||
setIsJson(true)
|
||||
return
|
||||
}
|
||||
data = showInput ? node.attributes.inputs : node.attributes.outputs
|
||||
|
||||
if (!showInput && node.status === 'ERROR') {
|
||||
data = node.events && Array.isArray(node.events) ? node.events?.find((e) => e.name === 'exception') : undefined
|
||||
}
|
||||
|
||||
if (typeof data === 'string' && (data.startsWith('{') || data.startsWith('['))) {
|
||||
try {
|
||||
setJsonData(JSON.parse(data))
|
||||
setIsJson(true)
|
||||
return
|
||||
} catch {
|
||||
console.error('failed to parse json data:', data)
|
||||
}
|
||||
} else if (typeof data === 'object' || Array.isArray(data)) {
|
||||
setJsonData(data)
|
||||
setIsJson(true)
|
||||
return
|
||||
}
|
||||
setIsJson(false)
|
||||
setJsonData(data as unknown as object)
|
||||
}, [node.attributes, node.status, node.events, showInput])
|
||||
|
||||
useEffect(() => {
|
||||
setUsedTime(convertTime((node.endTime || Date.now()) - node.startTime))
|
||||
changeJsonData()
|
||||
}, [node.endTime, node.startTime, node.attributes, node.events, changeJsonData])
|
||||
|
||||
useEffect(() => {
|
||||
const updateCopyButtonTitles = () => {
|
||||
const copyButtons = document.querySelectorAll('.copy-to-clipboard-container > span')
|
||||
copyButtons.forEach((btn) => {
|
||||
btn.setAttribute('title', t('code_block.copy'))
|
||||
})
|
||||
}
|
||||
|
||||
updateCopyButtonTitles()
|
||||
const timer = setInterval(updateCopyButtonTitles, 100) // 每秒检查一次
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [t])
|
||||
|
||||
const formatDate = (timestamp: number | null) => {
|
||||
if (timestamp == null) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(timestamp)
|
||||
const pad = (n: number) => n.toString().padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds())}`
|
||||
}
|
||||
|
||||
return (
|
||||
<Box padding={5}>
|
||||
<Box padding={0} style={{ marginBottom: 10 }}>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
clickShowModal(true)
|
||||
}}
|
||||
href={'#'}
|
||||
style={{ marginRight: 8, fontSize: '14px' }}>
|
||||
<DoubleLeftOutlined style={{ fontSize: '12px' }} />
|
||||
{t('trace.backList')}
|
||||
</a>
|
||||
</Box>
|
||||
<Text style={{ fontWeight: 'bold', fontSize: 14 }}>{t('trace.spanDetail')}</Text>
|
||||
<Box padding={0}>
|
||||
<Text style={{ fontWeight: 'bold' }}>ID: </Text>
|
||||
<Text>{node.id}</Text>
|
||||
</Box>
|
||||
<Box padding={0}>
|
||||
<Text style={{ fontWeight: 'bold' }}>{t('trace.name')}: </Text>
|
||||
<Text>{node.name}</Text>
|
||||
</Box>
|
||||
<Box padding={0}>
|
||||
<Text style={{ fontWeight: 'bold' }}>{t('trace.tag')}: </Text>
|
||||
<Text>{String(node.attributes?.tags || '')}</Text>
|
||||
</Box>
|
||||
<Box padding={0}>
|
||||
<Text style={{ fontWeight: 'bold' }}>{t('trace.startTime')}: </Text>
|
||||
<Text>{formatDate(node.startTime)}</Text>
|
||||
</Box>
|
||||
<Box padding={0}>
|
||||
<Text style={{ fontWeight: 'bold' }}>{t('trace.endTime')}: </Text>
|
||||
<Text>{formatDate(node.endTime)}</Text>
|
||||
</Box>
|
||||
{node.usage && (
|
||||
<Box padding={0}>
|
||||
<Text style={{ fontWeight: 'bold' }}>{t('trace.tokenUsage')}: </Text>
|
||||
<Text style={{ color: 'red' }}>{`↑${node.usage.prompt_tokens}`}</Text>
|
||||
<Text style={{ color: 'green' }}>{`↓${node.usage.completion_tokens}`}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box padding={0}>
|
||||
<Text style={{ fontWeight: 'bold' }}>{t('trace.spendTime')}: </Text>
|
||||
<Text>{usedTime}</Text>
|
||||
</Box>
|
||||
{/* <Box padding={0}>
|
||||
<Text style={{ fontWeight: 'bold' }}>{t('trace.parentId')}: </Text>
|
||||
<Text>{node.parentId}</Text>
|
||||
</Box> */}
|
||||
<Box style={{ position: 'relative', margin: '5px 0 0' }}>
|
||||
<Button className={`content-button ${showInput ? 'active' : ''}`} onClick={() => setShowInput(true)}>
|
||||
{t('trace.inputs')}
|
||||
</Button>
|
||||
<Button className={`content-button ${showInput ? '' : 'active'}`} onClick={() => setShowInput(false)}>
|
||||
{t('trace.outputs')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box className="code-container">
|
||||
{isJson ? (
|
||||
<ReactJson
|
||||
src={jsonData || ''}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={false}
|
||||
indentWidth={2}
|
||||
collapseStringsAfterLength={100}
|
||||
name={false}
|
||||
theme={'colors'}
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
) : (
|
||||
<pre
|
||||
style={{
|
||||
color: 'white',
|
||||
background: '#181c20',
|
||||
padding: '12px',
|
||||
borderRadius: 0,
|
||||
fontSize: 12,
|
||||
overflowX: 'auto',
|
||||
marginTop: '2px'
|
||||
}}>
|
||||
<code className="code-context">{`${typeof jsonData === 'object' ? JSON.stringify(jsonData, null, 2) : String(jsonData)}`}</code>
|
||||
</pre>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export default SpanDetail
|
||||
234
src/renderer/src/trace/pages/Trace.css
Normal file
234
src/renderer/src/trace/pages/Trace.css
Normal file
@ -0,0 +1,234 @@
|
||||
:root {
|
||||
--trace-bg: #fff;
|
||||
--trace-header-bg: #e5e5e5;
|
||||
--trace-border: #eee;
|
||||
--trace-text: #222;
|
||||
--trace-header: #eeeeee;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--trace-bg: #181c20;
|
||||
--trace-header-bg: #23272b;
|
||||
--trace-border: #333;
|
||||
--trace-text: #f1f1f1;
|
||||
--trace-header: #202125;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--trace-header);
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.headerIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: content;
|
||||
height: content;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
flex: 1;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
color: var(--trace-text);
|
||||
pointer-events: none;
|
||||
margin-left: 5px;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.traceItem {
|
||||
height: 32px;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.traceItem:hover {
|
||||
background-color: var(--trace-header-bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.headerOption {
|
||||
display: 'flex';
|
||||
align-items: 'center';
|
||||
gap: 8;
|
||||
width: 120;
|
||||
justify-content: 'flex-end';
|
||||
}
|
||||
|
||||
.tab-container_trace {
|
||||
background: var(--trace-bg);
|
||||
color: var(--trace-text);
|
||||
}
|
||||
|
||||
.scroll-container,
|
||||
.Box {
|
||||
background: var(--trace-bg);
|
||||
color: var(--trace-text);
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
background: var(--trace-header-bg);
|
||||
border-bottom: 1px solid var(--trace-border);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #d42817;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.floating {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 0px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0.9;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.default-text {
|
||||
color: val(--trace-text);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-header,
|
||||
.scroll-container,
|
||||
.Box {
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.tab-container_trace {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 20px 20px 10px;
|
||||
background: var(--ai-color-bg-primary);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
border-bottom: 2px solid #007bff;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
-webkit-app-region: no-drag;
|
||||
float: right;
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.code-container {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
margin-top: 2px;
|
||||
scrollbar-width: none;
|
||||
border-top: '1px solid #eee';
|
||||
width: '100%';
|
||||
}
|
||||
|
||||
.code-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-button {
|
||||
padding: 5px 10px 3px 10px;
|
||||
border: 1px solid black;
|
||||
border-radius: 10%;
|
||||
cursor: pointer;
|
||||
border-radius: 0;
|
||||
background: var(--ai-color-bg-primary);
|
||||
color: var(--trace-text);
|
||||
}
|
||||
|
||||
.content-button.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.settingsModal button[aria-label='Close'] {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.trace-window {
|
||||
width: 100%;
|
||||
height: calc(100vh - 77px);
|
||||
background-color: var(--trace-bg);
|
||||
color: var(--trace-text);
|
||||
opacity: 1;
|
||||
box-shadow: var(--trace-shadow);
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
font-size: 14px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 4px;
|
||||
background-color: var(--trace-header-bg);
|
||||
}
|
||||
|
||||
.code-context {
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.footer-link:hover {
|
||||
color: #062afb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
7
src/renderer/src/trace/pages/TraceModel.tsx
Normal file
7
src/renderer/src/trace/pages/TraceModel.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { SpanEntity } from '@mcp-trace/trace-core'
|
||||
|
||||
export interface TraceModal extends SpanEntity {
|
||||
children: TraceModal[]
|
||||
start: number
|
||||
percent: number
|
||||
}
|
||||
133
src/renderer/src/trace/pages/TraceTree.tsx
Normal file
133
src/renderer/src/trace/pages/TraceTree.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
||||
import { Divider } from 'antd/lib'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Box, GridItem, HStack, IconButton, SimpleGrid, Text } from './Component'
|
||||
import { ProgressBar } from './ProgressBar'
|
||||
|
||||
interface TreeNodeProps {
|
||||
node: TraceModal
|
||||
handleClick: (nodeId: string) => void
|
||||
treeData?: TraceModal[]
|
||||
paddingLeft?: number
|
||||
}
|
||||
|
||||
export const convertTime = (time: number | null): string => {
|
||||
if (time == null) {
|
||||
return ''
|
||||
}
|
||||
if (time > 100000) {
|
||||
return `${(time / 1000).toFixed(0)}s`
|
||||
}
|
||||
if (time > 10000) {
|
||||
return `${(time / 1000).toFixed(1)}s`
|
||||
}
|
||||
if (time > 1000) {
|
||||
return `${(time / 1000).toFixed(2)}s`
|
||||
}
|
||||
if (time > 100) {
|
||||
return `${time.toFixed(0)}ms`
|
||||
}
|
||||
if (time > 10) {
|
||||
return `${time.toFixed(1)}ms`
|
||||
}
|
||||
return time.toFixed(2) + 'ms'
|
||||
}
|
||||
|
||||
const TreeNode: React.FC<TreeNodeProps> = ({ node, handleClick, treeData, paddingLeft = 2 }) => {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
const hasChildren = node.children && node.children.length > 0
|
||||
const [usedTime, setUsedTime] = useState('--')
|
||||
|
||||
// 只在 endTime 或 node 变化时更新 usedTime
|
||||
useEffect(() => {
|
||||
const endTime = node.endTime || Date.now()
|
||||
setUsedTime(convertTime(endTime - node.startTime))
|
||||
}, [node])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%'
|
||||
}}>
|
||||
<SimpleGrid
|
||||
columns={20}
|
||||
className="traceItem"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleClick(node.id)
|
||||
}}>
|
||||
<GridItem colSpan={8} style={{ paddingLeft: `${paddingLeft}px`, textAlign: 'left' }}>
|
||||
<HStack grap={2}>
|
||||
<IconButton
|
||||
aria-label="Toggle"
|
||||
aria-expanded={isOpen ? true : false}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsOpen(!isOpen)
|
||||
}}
|
||||
fontSize="10px"
|
||||
style={{
|
||||
margin: '0px',
|
||||
visibility: hasChildren ? 'visible' : 'hidden'
|
||||
}}
|
||||
/>
|
||||
<Text role="button" tabIndex={0} className={node.status === 'ERROR' ? 'error-text' : 'default-text'}>
|
||||
{node.name}
|
||||
</Text>
|
||||
</HStack>
|
||||
</GridItem>
|
||||
{/* <GridItem padding={4} colSpan={3}>
|
||||
<Text
|
||||
// ml={2}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
{node.attributes?.tags}
|
||||
</Text>
|
||||
</GridItem> */}
|
||||
<GridItem colSpan={5}>
|
||||
<Text style={{ color: 'red' }}>{node.usage ? '↑' + node.usage.prompt_tokens : ''}</Text>
|
||||
<Text style={{ color: 'green' }}>{node.usage ? '↓' + node.usage.completion_tokens : ''}</Text>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Text /** ml={2} */>{usedTime}</Text>
|
||||
</GridItem>
|
||||
<GridItem padding={2} colSpan={4}>
|
||||
<ProgressBar progress={Math.max(node.percent, 5)} start={node.start} />
|
||||
</GridItem>
|
||||
</SimpleGrid>
|
||||
<Divider
|
||||
orientation="end"
|
||||
style={{
|
||||
borderTop: '1px solid #ccc',
|
||||
width: '100%',
|
||||
margin: '0px 5px 0px 0px'
|
||||
}}
|
||||
/>
|
||||
{hasChildren && isOpen && (
|
||||
<Box>
|
||||
{node.children &&
|
||||
node.children
|
||||
.sort((a, b) => a.startTime - b.startTime)
|
||||
.map((childNode) => (
|
||||
<TreeNode
|
||||
key={childNode.id}
|
||||
treeData={treeData}
|
||||
node={childNode}
|
||||
handleClick={handleClick}
|
||||
paddingLeft={paddingLeft + 4}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TreeNode
|
||||
200
src/renderer/src/trace/pages/index.tsx
Normal file
200
src/renderer/src/trace/pages/index.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import './Trace.css'
|
||||
|
||||
import { SpanEntity } from '@mcp-trace/trace-core'
|
||||
import { TraceModal } from '@renderer/trace/pages/TraceModel'
|
||||
import { Divider } from 'antd/lib'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Box, GridItem, SimpleGrid, Text, VStack } from './Component'
|
||||
import SpanDetail from './SpanDetail'
|
||||
import TraceTree from './TraceTree'
|
||||
|
||||
export interface TracePageProp {
|
||||
topicId: string
|
||||
traceId: string
|
||||
modelName?: string
|
||||
reload?: boolean
|
||||
}
|
||||
|
||||
export const TracePage: React.FC<TracePageProp> = ({ topicId, traceId, modelName, reload = false }) => {
|
||||
const [spans, setSpans] = useState<TraceModal[]>([])
|
||||
const [selectNode, setSelectNode] = useState<TraceModal | null>(null)
|
||||
const [showList, setShowList] = useState(true)
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const mergeTraceModals = useCallback((oldNodes: TraceModal[], newNodes: TraceModal[]): TraceModal[] => {
|
||||
const oldMap = new Map(oldNodes.map((n) => [n.id, n]))
|
||||
return newNodes.map((newNode) => {
|
||||
const oldNode = oldMap.get(newNode.id)
|
||||
if (oldNode) {
|
||||
// 如果旧节点已经结束,则直接返回旧节点
|
||||
if (oldNode.endTime) {
|
||||
return oldNode
|
||||
}
|
||||
oldNode.children = mergeTraceModals(oldNode.children, newNode.children)
|
||||
Object.assign(oldNode, newNode)
|
||||
return oldNode
|
||||
} else {
|
||||
return newNode
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const updatePercentAndStart = useCallback((nodes: TraceModal[], rootStart?: number, rootEnd?: number) => {
|
||||
nodes.forEach((node) => {
|
||||
const _rootStart = rootStart || node.startTime
|
||||
const _rootEnd = rootEnd || node.endTime || Date.now()
|
||||
const endTime = node.endTime || _rootEnd
|
||||
const usedTime = endTime - node.startTime
|
||||
const duration = _rootEnd - _rootStart
|
||||
node.start = ((node.startTime - _rootStart) * 100) / duration
|
||||
node.percent = duration === 0 ? 0 : (usedTime * 100) / duration
|
||||
if (node.children) {
|
||||
updatePercentAndStart(node.children, _rootStart, _rootEnd)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const getRootSpan = (spans: SpanEntity[]): TraceModal[] => {
|
||||
const map: Map<string, TraceModal> = new Map()
|
||||
|
||||
spans.map((span) => {
|
||||
map.set(span.id, { ...span, children: [], percent: 100, start: 0 })
|
||||
})
|
||||
|
||||
return Array.from(
|
||||
map.values().filter((span) => {
|
||||
if (span.parentId && map.has(span.parentId)) {
|
||||
const parent = map.get(span.parentId)
|
||||
if (parent) {
|
||||
parent.children.push(span)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const findNodeById = useCallback((nodes: TraceModal[], id: string): TraceModal | null => {
|
||||
for (const n of nodes) {
|
||||
if (n.id === id) return n
|
||||
if (n.children) {
|
||||
const found = findNodeById(n.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [])
|
||||
|
||||
const getTraceData = useCallback(async (): Promise<boolean> => {
|
||||
const datas = topicId && traceId ? await window.api.trace.getData(topicId, traceId, modelName) : []
|
||||
const matchedSpans = getRootSpan(datas)
|
||||
updatePercentAndStart(matchedSpans)
|
||||
setSpans((prev) => mergeTraceModals(prev, matchedSpans))
|
||||
const isEnded = !matchedSpans.find((e) => !e.endTime || e.endTime <= 0)
|
||||
return isEnded
|
||||
}, [topicId, traceId, modelName, updatePercentAndStart, mergeTraceModals])
|
||||
|
||||
const handleNodeClick = (nodeId: string) => {
|
||||
const latestNode = findNodeById(spans, nodeId)
|
||||
if (latestNode) {
|
||||
setSelectNode(latestNode)
|
||||
setShowList(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowList = () => {
|
||||
setShowList(true)
|
||||
setSelectNode(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleShowTrace = async () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
const ended = await getTraceData()
|
||||
// 只有未结束时才启动定时刷新
|
||||
if (!ended) {
|
||||
intervalRef.current = setInterval(async () => {
|
||||
const endedInner = await getTraceData()
|
||||
if (endedInner && intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
handleShowTrace()
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
}, [getTraceData, traceId, topicId, reload])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectNode) {
|
||||
const latest = findNodeById(spans, selectNode.id)
|
||||
if (!latest) {
|
||||
setShowList(true)
|
||||
setSelectNode(null)
|
||||
} else if (latest !== selectNode) {
|
||||
setSelectNode(latest)
|
||||
}
|
||||
}
|
||||
}, [spans, selectNode, findNodeById])
|
||||
|
||||
return (
|
||||
<div className="trace-window">
|
||||
<div className="tab-container_trace">
|
||||
<SimpleGrid columns={1} templateColumns="1fr">
|
||||
<Box padding={0} className="scroll-container">
|
||||
{showList ? (
|
||||
<VStack grap={1} align="start">
|
||||
{spans.length === 0 ? (
|
||||
<Text>没有找到Trace信息</Text>
|
||||
) : (
|
||||
<>
|
||||
<SimpleGrid columns={20} style={{ width: '100%' }} className="floating">
|
||||
<GridItem colSpan={8} padding={0} className={'table-header'}>
|
||||
<Text tabIndex={0}>{t('trace.name')}</Text>
|
||||
</GridItem>
|
||||
<GridItem colSpan={5} padding={0} className={'table-header'}>
|
||||
<Text>{t('trace.tokenUsage')}</Text>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3} padding={0} className={'table-header'}>
|
||||
<Text>{t('trace.spendTime')}</Text>
|
||||
</GridItem>
|
||||
<GridItem colSpan={4} padding={0} className={'table-header'}>
|
||||
<Text></Text>
|
||||
</GridItem>
|
||||
</SimpleGrid>
|
||||
<Divider
|
||||
orientation="end"
|
||||
style={{
|
||||
width: '100%',
|
||||
marginTop: '36px',
|
||||
marginBottom: '0px'
|
||||
}}
|
||||
/>
|
||||
{spans.map((node: TraceModal) => (
|
||||
<TraceTree key={node.id} treeData={node.children} node={node} handleClick={handleNodeClick} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
) : (
|
||||
selectNode && <SpanDetail node={selectNode} clickShowModal={handleShowList} />
|
||||
)}
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
66
src/renderer/src/trace/traceWindow.tsx
Normal file
66
src/renderer/src/trace/traceWindow.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import { TraceIcon } from './pages/Component'
|
||||
import { TracePage } from './pages/index'
|
||||
|
||||
const App = () => {
|
||||
const [traceId, setTraceId] = useState('')
|
||||
const [topicId, setTopicId] = useState('')
|
||||
const [modelName, setModelName] = useState<string | undefined>(undefined)
|
||||
const [reload, setReload] = useState(false)
|
||||
const [title, setTitle] = useState('Call Chain Window')
|
||||
|
||||
useEffect(() => {
|
||||
const setTraceHandler = (_, data) => {
|
||||
if (data?.traceId && data?.topicId) {
|
||||
setTraceId(data.traceId)
|
||||
setTopicId(data.topicId)
|
||||
setModelName(data.modelName)
|
||||
setReload(!reload)
|
||||
}
|
||||
}
|
||||
|
||||
const setLangHandler = (_, data) => {
|
||||
i18n.changeLanguage(data.lang)
|
||||
const newTitle = i18n.t('trace.traceWindow')
|
||||
if (newTitle !== title) {
|
||||
window.api.trace.setTraceWindowTitle(i18n.t('trace.traceWindow'))
|
||||
setTitle(newTitle)
|
||||
}
|
||||
}
|
||||
|
||||
const removeTraceHandler = window.electron.ipcRenderer.once('set-trace', setTraceHandler)
|
||||
const removeLanguageHandler = window.electron.ipcRenderer.once('set-language', setLangHandler)
|
||||
|
||||
return () => {
|
||||
removeTraceHandler()
|
||||
removeLanguageHandler()
|
||||
}
|
||||
}, [title, reload, modelName, traceId, topicId])
|
||||
|
||||
const handleFooterClick = () => {
|
||||
window.api.shell.openExternal('https://www.aliyun.com/product/edas')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="headerIcon">
|
||||
<TraceIcon color="#e74c3c" size={24} />
|
||||
</div>
|
||||
<div className="headerTitle">{title}</div>
|
||||
</header>
|
||||
<TracePage traceId={traceId} topicId={topicId} reload={reload} modelName={modelName} />
|
||||
<footer>
|
||||
<span onClick={handleFooterClick} className="footer-link">
|
||||
{i18n.t('trace.edasSupport')}
|
||||
</span>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const root = createRoot(document.getElementById('root')!)
|
||||
root.render(<App />)
|
||||
80
src/renderer/src/trace/types/ModelSpanEntity.ts
Normal file
80
src/renderer/src/trace/types/ModelSpanEntity.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { Span } from '@opentelemetry/api'
|
||||
|
||||
export interface StartSpanParams {
|
||||
topicId: string
|
||||
name?: string
|
||||
inputs?: any | any[]
|
||||
tag?: string
|
||||
parentSpanId?: string
|
||||
modelName?: string
|
||||
}
|
||||
|
||||
export interface EndSpanParams {
|
||||
topicId: string
|
||||
modelName?: string
|
||||
outputs?: any | any[]
|
||||
error?: Error
|
||||
span?: Span
|
||||
modelEnded?: boolean
|
||||
}
|
||||
|
||||
export class ModelSpanEntity {
|
||||
private modelName?: string
|
||||
private spans: Span[] = []
|
||||
private root?: Span
|
||||
|
||||
constructor(modelName?: string) {
|
||||
this.modelName = modelName
|
||||
}
|
||||
|
||||
getCurrentSpan(modelName?: string): Span | undefined {
|
||||
if (modelName !== this.modelName) return undefined
|
||||
return this.spans.length > 0 ? this.spans[this.spans.length - 1] : undefined
|
||||
}
|
||||
|
||||
getRoot(): Span | undefined {
|
||||
return this.root
|
||||
}
|
||||
|
||||
addSpan(span: Span, isRoot = false) {
|
||||
if (isRoot) {
|
||||
this.root = span
|
||||
}
|
||||
this.spans.push(span)
|
||||
}
|
||||
|
||||
removeSpan(span: Span) {
|
||||
const index = this.spans.indexOf(span)
|
||||
if (index !== -1) {
|
||||
this.spans.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
finishSpan() {
|
||||
this.spans.forEach((span) => {
|
||||
span.setAttribute('outputs', 'you paused')
|
||||
span.end()
|
||||
})
|
||||
this.spans = []
|
||||
}
|
||||
|
||||
getModelName() {
|
||||
return this.modelName
|
||||
}
|
||||
|
||||
getRootSpan() {
|
||||
return this.spans && this.spans.length > 0 ? this.spans[0] : undefined
|
||||
}
|
||||
|
||||
getSpanById(spanId?: string) {
|
||||
return spanId ? this.spans.find((span) => span.spanContext().spanId === spanId) : undefined
|
||||
}
|
||||
|
||||
addModelError(error: Error) {
|
||||
this.spans.forEach((span) => {
|
||||
span.recordException(error, Date.now())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -545,6 +545,9 @@ export type WebSearchProvider = {
|
||||
basicAuthUsername?: string
|
||||
basicAuthPassword?: string
|
||||
usingBrowser?: boolean
|
||||
topicId?: string
|
||||
parentSpanId?: string
|
||||
modelName?: string
|
||||
}
|
||||
|
||||
export type WebSearchProviderResult = {
|
||||
|
||||
@ -189,6 +189,9 @@ export type Message = {
|
||||
|
||||
// 块集合
|
||||
blocks: MessageBlock['id'][]
|
||||
|
||||
// 跟踪Id
|
||||
traceId?: string
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
|
||||
@ -3,6 +3,7 @@ import { Content, FunctionCall, Part, Tool, Type as GeminiSchemaType } from '@go
|
||||
import { loggerService } from '@logger'
|
||||
import { isFunctionCallingModel, isVisionModel } from '@renderer/config/models'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { currentSpan } from '@renderer/services/SpanManagerService'
|
||||
import store from '@renderer/store'
|
||||
import { addMCPServer } from '@renderer/store/mcp'
|
||||
import {
|
||||
@ -268,7 +269,11 @@ export function openAIToolsToMcpTool(
|
||||
return tool
|
||||
}
|
||||
|
||||
export async function callMCPTool(toolResponse: MCPToolResponse): Promise<MCPCallToolResponse> {
|
||||
export async function callMCPTool(
|
||||
toolResponse: MCPToolResponse,
|
||||
topicId?: string,
|
||||
modelName?: string
|
||||
): Promise<MCPCallToolResponse> {
|
||||
logger.info(`Calling Tool: ${toolResponse.tool.serverName} ${toolResponse.tool.name}`, toolResponse.tool)
|
||||
try {
|
||||
const server = getMcpServerByTool(toolResponse.tool)
|
||||
@ -277,12 +282,15 @@ export async function callMCPTool(toolResponse: MCPToolResponse): Promise<MCPCal
|
||||
throw new Error(`Server not found: ${toolResponse.tool.serverName}`)
|
||||
}
|
||||
|
||||
const resp = await window.api.mcp.callTool({
|
||||
server,
|
||||
name: toolResponse.tool.name,
|
||||
args: toolResponse.arguments,
|
||||
callId: toolResponse.id
|
||||
})
|
||||
const resp = await window.api.mcp.callTool(
|
||||
{
|
||||
server,
|
||||
name: toolResponse.tool.name,
|
||||
args: toolResponse.arguments,
|
||||
callId: toolResponse.id
|
||||
},
|
||||
topicId ? currentSpan(topicId, modelName)?.spanContext() : undefined
|
||||
)
|
||||
if (toolResponse.tool.serverName === MCP_AUTO_INSTALL_SERVER_NAME) {
|
||||
if (resp.data) {
|
||||
const mcpServer: MCPServer = {
|
||||
@ -533,7 +541,8 @@ export async function parseAndCallTools<R>(
|
||||
convertToMessage: (mcpToolResponse: MCPToolResponse, resp: MCPCallToolResponse, model: Model) => R | undefined,
|
||||
model: Model,
|
||||
mcpTools?: MCPTool[],
|
||||
abortSignal?: AbortSignal
|
||||
abortSignal?: AbortSignal,
|
||||
topicId?: CompletionsParams['topicId']
|
||||
): Promise<{ toolResults: R[]; confirmedToolResponses: MCPToolResponse[] }>
|
||||
|
||||
export async function parseAndCallTools<R>(
|
||||
@ -543,7 +552,8 @@ export async function parseAndCallTools<R>(
|
||||
convertToMessage: (mcpToolResponse: MCPToolResponse, resp: MCPCallToolResponse, model: Model) => R | undefined,
|
||||
model: Model,
|
||||
mcpTools?: MCPTool[],
|
||||
abortSignal?: AbortSignal
|
||||
abortSignal?: AbortSignal,
|
||||
topicId?: CompletionsParams['topicId']
|
||||
): Promise<{ toolResults: R[]; confirmedToolResponses: MCPToolResponse[] }>
|
||||
|
||||
export async function parseAndCallTools<R>(
|
||||
@ -553,7 +563,8 @@ export async function parseAndCallTools<R>(
|
||||
convertToMessage: (mcpToolResponse: MCPToolResponse, resp: MCPCallToolResponse, model: Model) => R | undefined,
|
||||
model: Model,
|
||||
mcpTools?: MCPTool[],
|
||||
abortSignal?: AbortSignal
|
||||
abortSignal?: AbortSignal,
|
||||
topicId?: CompletionsParams['topicId']
|
||||
): Promise<{ toolResults: R[]; confirmedToolResponses: MCPToolResponse[] }> {
|
||||
const toolResults: R[] = []
|
||||
let curToolResponses: MCPToolResponse[] = []
|
||||
@ -616,7 +627,7 @@ export async function parseAndCallTools<R>(
|
||||
// 执行工具调用
|
||||
try {
|
||||
const images: string[] = []
|
||||
const toolCallResponse = await callMCPTool(toolResponse)
|
||||
const toolCallResponse = await callMCPTool(toolResponse, topicId, model.name)
|
||||
|
||||
// 立即更新为done状态
|
||||
upsertMCPToolResponse(
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { endTrace } from '@renderer/services/SpanManagerService'
|
||||
import PQueue from 'p-queue'
|
||||
|
||||
// Queue configuration - managed by topic
|
||||
@ -10,7 +11,11 @@ const requestQueues: { [topicId: string]: PQueue } = {}
|
||||
* @returns A PQueue instance for the topic
|
||||
*/
|
||||
export const getTopicQueue = (topicId: string, options = {}): PQueue => {
|
||||
if (!requestQueues[topicId]) requestQueues[topicId] = new PQueue(options)
|
||||
if (!requestQueues[topicId]) {
|
||||
requestQueues[topicId] = new PQueue(options).addListener('idle', () => {
|
||||
endTrace({ topicId })
|
||||
})
|
||||
}
|
||||
return requestQueues[topicId]
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
getDefaultModel,
|
||||
getDefaultTopic
|
||||
} from '@renderer/services/AssistantService'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import type { ActionItem } from '@renderer/types/selectionTypes'
|
||||
import { abortCompletion } from '@renderer/utils/abortController'
|
||||
@ -140,6 +141,9 @@ const ActionGeneral: FC<Props> = React.memo(({ action, scrollToBottom }) => {
|
||||
abortCompletion(askId.current)
|
||||
setIsLoading(false)
|
||||
}
|
||||
if (topicRef.current?.id) {
|
||||
pauseTrace(topicRef.current.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegenerate = () => {
|
||||
|
||||
40
src/renderer/traceWindow.html
Normal file
40
src/renderer/traceWindow.html
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#root {
|
||||
flex: 1 1 auto;
|
||||
background: var(--trace-bg);
|
||||
}
|
||||
footer {
|
||||
height: 36px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: grey;
|
||||
background: var(--trace-header-bg);
|
||||
flex-shrink: 0;
|
||||
line-height: 36px;
|
||||
position: relative;
|
||||
z-index: 1
|
||||
}
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/trace/traceWindow.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"tsDecorders": "legacy",
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
@ -7,5 +8,10 @@
|
||||
{
|
||||
"path": "./tsconfig.web.json"
|
||||
}
|
||||
]
|
||||
],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,9 @@
|
||||
"src/main/env.d.ts",
|
||||
"src/renderer/src/types/*",
|
||||
"packages/shared/**/*",
|
||||
"scripts"
|
||||
],
|
||||
"scripts",
|
||||
"packages/mcp-trace/**/*",
|
||||
"src/renderer/src/services/traceApi.ts" ],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": [
|
||||
@ -20,7 +21,11 @@
|
||||
"@main/*": ["src/main/*"],
|
||||
"@types": ["src/renderer/src/types/index.ts"],
|
||||
"@shared/*": ["packages/shared/*"],
|
||||
"@logger": ["src/main/services/LoggerService"]
|
||||
}
|
||||
"@logger": ["src/main/services/LoggerService"],
|
||||
"@mcp-trace/*": ["packages/mcp-trace/*"]
|
||||
},
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
"src/preload/*.d.ts",
|
||||
"local/src/renderer/**/*",
|
||||
"packages/shared/**/*",
|
||||
"tests/__mocks__/**/*"
|
||||
"tests/__mocks__/**/*",
|
||||
"packages/mcp-trace/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
@ -16,7 +17,11 @@
|
||||
"@renderer/*": ["src/renderer/src/*"],
|
||||
"@shared/*": ["packages/shared/*"],
|
||||
"@types": ["src/renderer/src/types/index.ts"],
|
||||
"@logger": ["src/renderer/src/services/LoggerService"]
|
||||
}
|
||||
"@logger": ["src/renderer/src/services/LoggerService"],
|
||||
"@mcp-trace/*": ["packages/mcp-trace/*"]
|
||||
},
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": true
|
||||
}
|
||||
}
|
||||
|
||||
578
yarn.lock
578
yarn.lock
@ -872,7 +872,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.26.2":
|
||||
"@babel/code-frame@npm:^7.26.2":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/code-frame@npm:7.27.1"
|
||||
dependencies:
|
||||
@ -962,10 +962,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-plugin-utils@npm:^7.25.9":
|
||||
version: 7.26.5
|
||||
resolution: "@babel/helper-plugin-utils@npm:7.26.5"
|
||||
checksum: 10c0/cdaba71d4b891aa6a8dfbe5bac2f94effb13e5fa4c2c487667fdbaa04eae059b78b28d85a885071f45f7205aeb56d16759e1bed9c118b94b16e4720ef1ab0f65
|
||||
"@babel/helper-plugin-utils@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-plugin-utils@npm:7.27.1"
|
||||
checksum: 10c0/94cf22c81a0c11a09b197b41ab488d416ff62254ce13c57e62912c85700dc2e99e555225787a4099ff6bae7a1812d622c80fbaeda824b79baa10a6c5ac4cf69b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -1012,13 +1012,13 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-arrow-functions@npm:^7.25.9":
|
||||
version: 7.25.9
|
||||
resolution: "@babel/plugin-transform-arrow-functions@npm:7.25.9"
|
||||
version: 7.27.1
|
||||
resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.25.9"
|
||||
"@babel/helper-plugin-utils": "npm:^7.27.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/851fef9f58be60a80f46cc0ce1e46a6f7346a6f9d50fa9e0fa79d46ec205320069d0cc157db213e2bea88ef5b7d9bd7618bb83f0b1996a836e2426c3a3a1f622
|
||||
checksum: 10c0/19abd7a7d11eef58c9340408a4c2594503f6c4eaea1baa7b0e5fbdda89df097e50663edb3448ad2300170b39efca98a75e5767af05cad3b0facb4944326896a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -1029,6 +1029,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.20.13":
|
||||
version: 7.27.4
|
||||
resolution: "@babel/runtime@npm:7.27.4"
|
||||
checksum: 10c0/ca99e964179c31615e1352e058cc9024df7111c829631c90eec84caba6703cc32acc81503771847c306b3c70b815609fe82dde8682936debe295b0b283b2dc6e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.26.9, @babel/template@npm:^7.27.0":
|
||||
version: 7.27.0
|
||||
resolution: "@babel/template@npm:7.27.0"
|
||||
@ -1055,7 +1062,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0, @babel/types@npm:^7.27.7":
|
||||
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/types@npm:7.27.1"
|
||||
dependencies:
|
||||
"@babel/helper-string-parser": "npm:^7.27.1"
|
||||
"@babel/helper-validator-identifier": "npm:^7.27.1"
|
||||
checksum: 10c0/ed736f14db2fdf0d36c539c8e06b6bb5e8f9649a12b5c0e1c516fed827f27ef35085abe08bf4d1302a4e20c9a254e762eed453bce659786d4a6e01ba26a91377
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0":
|
||||
version: 7.27.0
|
||||
resolution: "@babel/types@npm:7.27.0"
|
||||
dependencies:
|
||||
"@babel/helper-string-parser": "npm:^7.25.9"
|
||||
"@babel/helper-validator-identifier": "npm:^7.25.9"
|
||||
checksum: 10c0/6f1592eabe243c89a608717b07b72969be9d9d2fce1dee21426238757ea1fa60fdfc09b29de9e48d8104311afc6e6fb1702565a9cc1e09bc1e76f2b2ddb0f6e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.27.7":
|
||||
version: 7.27.7
|
||||
resolution: "@babel/types@npm:7.27.7"
|
||||
dependencies:
|
||||
@ -3773,6 +3800,204 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/api-logs@npm:0.200.0":
|
||||
version: 0.200.0
|
||||
resolution: "@opentelemetry/api-logs@npm:0.200.0"
|
||||
dependencies:
|
||||
"@opentelemetry/api": "npm:^1.3.0"
|
||||
checksum: 10c0/c6bc3cfba35c69411f294519d93d0ff9f603517030d1162839ee42ac22ed1b0235edaf71d00cabc40125f813d8b4dc830d14315afcebcef138c1df560eaa5c91
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0":
|
||||
version: 1.9.0
|
||||
resolution: "@opentelemetry/api@npm:1.9.0"
|
||||
checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/context-async-hooks@npm:2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@opentelemetry/context-async-hooks@npm:2.0.1"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 10c0/75b06f33b9c3dccb8d9802c14badcc3b9a497b21c77bf0344fc6231041ea1bf6a2bcc195cc27fafd5914bffcc7fa160b9f4480c06a37e86e876c98bf1a533a0d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/core@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@opentelemetry/core@npm:2.0.0"
|
||||
dependencies:
|
||||
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 10c0/d2cc6d8a955305b9de15cc36135e5d5b0f0405fead8bbd4de51433f2d05369af0a3bcb2c6fe7fe6d9e61b0db782511bcadc5d93ed906027d4c00d5c2e3575a24
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/core@npm:2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@opentelemetry/core@npm:2.0.1"
|
||||
dependencies:
|
||||
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 10c0/d587b1289559757d80da98039f9f57612f84f72ec608cd665dc467c7c6c5ce3a987dfcc2c63b521c7c86ce984a2552b3ead15a0dc458de1cf6bde5cdfe4ca9d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-http@npm:^0.200.0":
|
||||
version: 0.200.0
|
||||
resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.200.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:2.0.0"
|
||||
"@opentelemetry/otlp-exporter-base": "npm:0.200.0"
|
||||
"@opentelemetry/otlp-transformer": "npm:0.200.0"
|
||||
"@opentelemetry/resources": "npm:2.0.0"
|
||||
"@opentelemetry/sdk-trace-base": "npm:2.0.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ^1.3.0
|
||||
checksum: 10c0/9cc914570cca1bd411e467f0a03146d3397c7940c8f9f5f876a28f9c8345f1b0d433651df8c8a0006d13c8b62f0d04ef44a7f7419d2765fcd061f1cbd585b6c5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/otlp-exporter-base@npm:0.200.0":
|
||||
version: 0.200.0
|
||||
resolution: "@opentelemetry/otlp-exporter-base@npm:0.200.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:2.0.0"
|
||||
"@opentelemetry/otlp-transformer": "npm:0.200.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ^1.3.0
|
||||
checksum: 10c0/3283c12bffc3156a41d9c16c097966e8418781a1d779250334f3d5b4f864be1aeac69fecfdf489abc95578dc36098dc0e026e5a48eb19ee170d72ef89b94f0e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/otlp-transformer@npm:0.200.0":
|
||||
version: 0.200.0
|
||||
resolution: "@opentelemetry/otlp-transformer@npm:0.200.0"
|
||||
dependencies:
|
||||
"@opentelemetry/api-logs": "npm:0.200.0"
|
||||
"@opentelemetry/core": "npm:2.0.0"
|
||||
"@opentelemetry/resources": "npm:2.0.0"
|
||||
"@opentelemetry/sdk-logs": "npm:0.200.0"
|
||||
"@opentelemetry/sdk-metrics": "npm:2.0.0"
|
||||
"@opentelemetry/sdk-trace-base": "npm:2.0.0"
|
||||
protobufjs: "npm:^7.3.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ^1.3.0
|
||||
checksum: 10c0/4f5383fad48c62e17824df91f6944b0376cb17f7b132b11d62fa5cf46747f224c980960209c85669b6e341a131f94586c6ad52bc1a6d2fb8d5295e23b460600c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/resources@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@opentelemetry/resources@npm:2.0.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:2.0.0"
|
||||
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
checksum: 10c0/2f331ff8268ef7168e8f24312fd7505900693c0ea302f6025937e94c157b8173ee54f5d5a737c06b956da721aa63443ac520f530cade880ef3cd40a2a25c702c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/resources@npm:2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@opentelemetry/resources@npm:2.0.1"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:2.0.1"
|
||||
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
checksum: 10c0/96532b7553b26607a7a892d72f6b03ad12bd542dc23c95135a8ae40362da9c883c21a4cff3d2296d9e0e9bd899a5977e325ed52d83142621a8ffe81d08d99341
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/sdk-logs@npm:0.200.0":
|
||||
version: 0.200.0
|
||||
resolution: "@opentelemetry/sdk-logs@npm:0.200.0"
|
||||
dependencies:
|
||||
"@opentelemetry/api-logs": "npm:0.200.0"
|
||||
"@opentelemetry/core": "npm:2.0.0"
|
||||
"@opentelemetry/resources": "npm:2.0.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.4.0 <1.10.0"
|
||||
checksum: 10c0/031dc40dd012fad102e5c8c0c9bdbbce051dbc7fcc2e05e003f959aeb34d252dc3595b353ea2a9f900ff40f45d19cb4c8f7ab95a9faa01391f6b415c7780c786
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/sdk-metrics@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@opentelemetry/sdk-metrics@npm:2.0.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:2.0.0"
|
||||
"@opentelemetry/resources": "npm:2.0.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.9.0 <1.10.0"
|
||||
checksum: 10c0/9a3c87738671f29a496a39d65b3ab0829b52d0f31c0be662ea575a8f77bc5444044fd01513c891abdff6bf6344a08730e18f79253a85e68962669f3e1fa12e72
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/sdk-trace-base@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@opentelemetry/sdk-trace-base@npm:2.0.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:2.0.0"
|
||||
"@opentelemetry/resources": "npm:2.0.0"
|
||||
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
checksum: 10c0/c63cc052741e4cc01d084c883e24a1c0792f081a242e14e5cf526d5a3d96bac5974006fa0d8f902bd04f34ed9ce95a0d0f01b7fdb37fcc813cea9f818f2b8f43
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/sdk-trace-base@npm:2.0.1, @opentelemetry/sdk-trace-base@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "@opentelemetry/sdk-trace-base@npm:2.0.1"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:2.0.1"
|
||||
"@opentelemetry/resources": "npm:2.0.1"
|
||||
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
checksum: 10c0/4e3c733296012b758d007e9c0d8a5b175edbe9a680c73ec75303476e7982b73ad4209f1a2791c1a94c428e5a53eba6c2a72faa430c70336005aa58744d6cb37b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/sdk-trace-node@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "@opentelemetry/sdk-trace-node@npm:2.0.1"
|
||||
dependencies:
|
||||
"@opentelemetry/context-async-hooks": "npm:2.0.1"
|
||||
"@opentelemetry/core": "npm:2.0.1"
|
||||
"@opentelemetry/sdk-trace-base": "npm:2.0.1"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 10c0/b237efc219dc10c33746c05461c8c8741edbe7558eaf7f2dab01a3e75af4788bfd0633a049cd5dc7ecf015a2de7aa948c3989c0131d1f140109fb5e7b0313d7a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/sdk-trace-web@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "@opentelemetry/sdk-trace-web@npm:2.0.1"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:2.0.1"
|
||||
"@opentelemetry/sdk-trace-base": "npm:2.0.1"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 10c0/48821b91430e24378b0b5b2632e78efdd018a3f840462a6aeba6ce318a6480bad2f623cc7f7f625a9266028ad44b78eb8456181778de6cb18725f26c44e2729b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/semantic-conventions@npm:^1.29.0":
|
||||
version: 1.34.0
|
||||
resolution: "@opentelemetry/semantic-conventions@npm:1.34.0"
|
||||
checksum: 10c0/a51a32a5cf5c803bd2125a680d0abacbff632f3b255d0fe52379dac191114a0e8d72a34f9c46c5483ccfe91c4061c309f3cf61a19d11347e2a69779e82cfefd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@parcel/watcher-android-arm64@npm:2.5.1":
|
||||
version: 2.5.1
|
||||
resolution: "@parcel/watcher-android-arm64@npm:2.5.1"
|
||||
@ -3949,6 +4174,79 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "@protobufjs/aspromise@npm:1.1.2"
|
||||
checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/base64@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "@protobufjs/base64@npm:1.1.2"
|
||||
checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/codegen@npm:^2.0.4":
|
||||
version: 2.0.4
|
||||
resolution: "@protobufjs/codegen@npm:2.0.4"
|
||||
checksum: 10c0/26ae337c5659e41f091606d16465bbcc1df1f37cc1ed462438b1f67be0c1e28dfb2ca9f294f39100c52161aef82edf758c95d6d75650a1ddf31f7ddee1440b43
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/eventemitter@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@protobufjs/eventemitter@npm:1.1.0"
|
||||
checksum: 10c0/1eb0a75180e5206d1033e4138212a8c7089a3d418c6dfa5a6ce42e593a4ae2e5892c4ef7421f38092badba4040ea6a45f0928869989411001d8c1018ea9a6e70
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/fetch@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@protobufjs/fetch@npm:1.1.0"
|
||||
dependencies:
|
||||
"@protobufjs/aspromise": "npm:^1.1.1"
|
||||
"@protobufjs/inquire": "npm:^1.1.0"
|
||||
checksum: 10c0/cda6a3dc2d50a182c5865b160f72077aac197046600091dbb005dd0a66db9cce3c5eaed6d470ac8ed49d7bcbeef6ee5f0bc288db5ff9a70cbd003e5909065233
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/float@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "@protobufjs/float@npm:1.0.2"
|
||||
checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/inquire@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@protobufjs/inquire@npm:1.1.0"
|
||||
checksum: 10c0/64372482efcba1fb4d166a2664a6395fa978b557803857c9c03500e0ac1013eb4b1aacc9ed851dd5fc22f81583670b4f4431bae186f3373fedcfde863ef5921a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/path@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "@protobufjs/path@npm:1.1.2"
|
||||
checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/pool@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@protobufjs/pool@npm:1.1.0"
|
||||
checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@protobufjs/utf8@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@protobufjs/utf8@npm:1.1.0"
|
||||
checksum: 10c0/a3fe31fe3fa29aa3349e2e04ee13dc170cc6af7c23d92ad49e3eeaf79b9766264544d3da824dba93b7855bd6a2982fb40032ef40693da98a136d835752beb487
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rc-component/async-validator@npm:^5.0.3":
|
||||
version: 5.0.4
|
||||
resolution: "@rc-component/async-validator@npm:5.0.4"
|
||||
@ -5796,6 +6094,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:>=13.7.0":
|
||||
version: 22.15.29
|
||||
resolution: "@types/node@npm:22.15.29"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.21.0"
|
||||
checksum: 10c0/602cc88c6150780cd9b5b44604754e0ce13983ae876a538861d6ecfb1511dff289e5576fffd26c841cde2142418d4bb76e2a72a382b81c04557ccb17cff29e1d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.11.18, @types/node@npm:^18.19.9":
|
||||
version: 18.19.86
|
||||
resolution: "@types/node@npm:18.19.86"
|
||||
@ -6881,6 +7188,12 @@ __metadata:
|
||||
"@modelcontextprotocol/sdk": "npm:^1.12.3"
|
||||
"@mozilla/readability": "npm:^0.6.0"
|
||||
"@notionhq/client": "npm:^2.2.15"
|
||||
"@opentelemetry/api": "npm:^1.9.0"
|
||||
"@opentelemetry/core": "npm:2.0.0"
|
||||
"@opentelemetry/exporter-trace-otlp-http": "npm:^0.200.0"
|
||||
"@opentelemetry/sdk-trace-base": "npm:^2.0.0"
|
||||
"@opentelemetry/sdk-trace-node": "npm:^2.0.0"
|
||||
"@opentelemetry/sdk-trace-web": "npm:^2.0.0"
|
||||
"@playwright/test": "npm:^1.52.0"
|
||||
"@reduxjs/toolkit": "npm:^2.2.5"
|
||||
"@shikijs/markdown-it": "npm:^3.7.0"
|
||||
@ -6970,7 +7283,7 @@ __metadata:
|
||||
node-stream-zip: "npm:^1.15.0"
|
||||
notion-helper: "npm:^1.3.22"
|
||||
npx-scope-finder: "npm:^1.2.0"
|
||||
officeparser: "npm:^4.1.1"
|
||||
officeparser: "npm:^4.2.0"
|
||||
openai: "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch"
|
||||
os-proxy-config: "npm:^1.1.2"
|
||||
p-queue: "npm:^8.1.0"
|
||||
@ -6985,6 +7298,7 @@ __metadata:
|
||||
react-hotkeys-hook: "npm:^4.6.1"
|
||||
react-i18next: "npm:^14.1.2"
|
||||
react-infinite-scroll-component: "npm:^6.1.0"
|
||||
react-json-view: "npm:^1.21.3"
|
||||
react-markdown: "npm:^10.1.0"
|
||||
react-redux: "npm:^9.1.2"
|
||||
react-router: "npm:6"
|
||||
@ -6993,6 +7307,7 @@ __metadata:
|
||||
react-window: "npm:^1.8.11"
|
||||
redux: "npm:^5.0.1"
|
||||
redux-persist: "npm:^6.0.0"
|
||||
reflect-metadata: "npm:0.2.2"
|
||||
rehype-katex: "npm:^7.0.1"
|
||||
rehype-mathjax: "npm:^7.1.0"
|
||||
rehype-raw: "npm:^7.0.0"
|
||||
@ -7515,6 +7830,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"asap@npm:~2.0.3":
|
||||
version: 2.0.6
|
||||
resolution: "asap@npm:2.0.6"
|
||||
checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"assert-plus@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "assert-plus@npm:1.0.0"
|
||||
@ -7642,6 +7964,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base16@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "base16@npm:1.0.0"
|
||||
checksum: 10c0/af1aee7b297d968528ef47c8de2c5274029743e8a4a5f61ec823e36b673781691d124168cb22936c7997f53d89b344c58bf7ecf93eeb148cffa7e3fb4e4b8b18
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
@ -8802,6 +9131,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cross-fetch@npm:^3.1.5":
|
||||
version: 3.2.0
|
||||
resolution: "cross-fetch@npm:3.2.0"
|
||||
dependencies:
|
||||
node-fetch: "npm:^2.7.0"
|
||||
checksum: 10c0/d8596adf0269130098a676f6739a0922f3cc7b71cc89729925411ebe851a87026171c82ea89154c4811c9867c01c44793205a52e618ce2684650218c7fbeeb9f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6":
|
||||
version: 7.0.6
|
||||
resolution: "cross-spawn@npm:7.0.6"
|
||||
@ -10992,6 +11330,37 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fbemitter@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "fbemitter@npm:3.0.0"
|
||||
dependencies:
|
||||
fbjs: "npm:^3.0.0"
|
||||
checksum: 10c0/f130dd8e15dc3fc6709a26586b7a589cd994e1d1024b624f2cc8ef1b12401536a94bb30038e68150a24f9ba18863e9a3fe87941ade2c87667bfbd17f4848d5c7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fbjs-css-vars@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "fbjs-css-vars@npm:1.0.2"
|
||||
checksum: 10c0/dfb64116b125a64abecca9e31477b5edb9a2332c5ffe74326fe36e0a72eef7fc8a49b86adf36c2c293078d79f4524f35e80f5e62546395f53fb7c9e69821f54f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fbjs@npm:^3.0.0, fbjs@npm:^3.0.1":
|
||||
version: 3.0.5
|
||||
resolution: "fbjs@npm:3.0.5"
|
||||
dependencies:
|
||||
cross-fetch: "npm:^3.1.5"
|
||||
fbjs-css-vars: "npm:^1.0.0"
|
||||
loose-envify: "npm:^1.0.0"
|
||||
object-assign: "npm:^4.1.0"
|
||||
promise: "npm:^7.1.1"
|
||||
setimmediate: "npm:^1.0.5"
|
||||
ua-parser-js: "npm:^1.0.35"
|
||||
checksum: 10c0/66d0a2fc9a774f9066e35ac2ac4bf1245931d27f3ac287c7d47e6aa1fc152b243c2109743eb8f65341e025621fb51a12038fadb9fd8fda2e3ddae04ebab06f91
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fd-slicer@npm:~1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "fd-slicer@npm:1.1.0"
|
||||
@ -11217,6 +11586,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"flux@npm:^4.0.1":
|
||||
version: 4.0.4
|
||||
resolution: "flux@npm:4.0.4"
|
||||
dependencies:
|
||||
fbemitter: "npm:^3.0.0"
|
||||
fbjs: "npm:^3.0.1"
|
||||
peerDependencies:
|
||||
react: ^15.0.2 || ^16.0.0 || ^17.0.0
|
||||
checksum: 10c0/948bc01b97ff21babc8bfe5c40543d643ca126b71edd447a9ac051b05d20fd549a6bcc4afe043bcde56201782e688a5eaeda1bd8e3e58915641abdd5b3ea21e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"follow-redirects@npm:^1.15.6":
|
||||
version: 1.15.9
|
||||
resolution: "follow-redirects@npm:1.15.9"
|
||||
@ -12805,7 +13186,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-tokens@npm:^4.0.0":
|
||||
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "js-tokens@npm:4.0.0"
|
||||
checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed
|
||||
@ -13414,6 +13795,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.curry@npm:^4.0.1":
|
||||
version: 4.1.1
|
||||
resolution: "lodash.curry@npm:4.1.1"
|
||||
checksum: 10c0/f0431947dc9236df879fc13eb40c31a2839c958bd0eaa39170a5758c25a7d85d461716a851ab45a175371950b283480615cdd4b07fb0dd1afff7a2914a90696f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.escaperegexp@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "lodash.escaperegexp@npm:4.1.2"
|
||||
@ -13421,6 +13809,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.flow@npm:^3.3.0":
|
||||
version: 3.5.0
|
||||
resolution: "lodash.flow@npm:3.5.0"
|
||||
checksum: 10c0/b3202ddbb79e5aab41719806d0d5ae969f64ae6b59e6bdaaecaa96ec68d6ba429e544017fe0e71ecf5b7ee3cea7b45d43c46b7d67ca159d6cca86fca76c61a31
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.isequal@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.isequal@npm:4.5.0"
|
||||
@ -13479,6 +13874,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"long@npm:^5.0.0":
|
||||
version: 5.3.2
|
||||
resolution: "long@npm:5.3.2"
|
||||
checksum: 10c0/7130fe1cbce2dca06734b35b70d380ca3f70271c7f8852c922a7c62c86c4e35f0c39290565eca7133c625908d40e126ac57c02b1b1a4636b9457d77e1e60b981
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"longest-streak@npm:^2.0.0":
|
||||
version: 2.0.4
|
||||
resolution: "longest-streak@npm:2.0.4"
|
||||
@ -13493,6 +13895,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"loose-envify@npm:^1.0.0":
|
||||
version: 1.4.0
|
||||
resolution: "loose-envify@npm:1.4.0"
|
||||
dependencies:
|
||||
js-tokens: "npm:^3.0.0 || ^4.0.0"
|
||||
bin:
|
||||
loose-envify: cli.js
|
||||
checksum: 10c0/655d110220983c1a4b9c0c679a2e8016d4b67f6e9c7b5435ff5979ecdb20d0813f4dec0a08674fcbdd4846a3f07edbb50a36811fd37930b94aaa0d9daceb017e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lop@npm:^0.4.1":
|
||||
version: 0.4.2
|
||||
resolution: "lop@npm:0.4.2"
|
||||
@ -15261,7 +15674,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9":
|
||||
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0":
|
||||
version: 2.7.0
|
||||
resolution: "node-fetch@npm:2.7.0"
|
||||
dependencies:
|
||||
@ -15480,7 +15893,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"officeparser@npm:^4.1.1":
|
||||
"officeparser@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "officeparser@npm:4.2.0"
|
||||
dependencies:
|
||||
@ -16394,6 +16807,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"promise@npm:^7.1.1":
|
||||
version: 7.3.1
|
||||
resolution: "promise@npm:7.3.1"
|
||||
dependencies:
|
||||
asap: "npm:~2.0.3"
|
||||
checksum: 10c0/742e5c0cc646af1f0746963b8776299701ad561ce2c70b49365d62c8db8ea3681b0a1bf0d4e2fe07910bf72f02d39e51e8e73dc8d7503c3501206ac908be107f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"property-information@npm:^6.0.0":
|
||||
version: 6.5.0
|
||||
resolution: "property-information@npm:6.5.0"
|
||||
@ -16408,6 +16830,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"protobufjs@npm:^7.3.0":
|
||||
version: 7.5.2
|
||||
resolution: "protobufjs@npm:7.5.2"
|
||||
dependencies:
|
||||
"@protobufjs/aspromise": "npm:^1.1.2"
|
||||
"@protobufjs/base64": "npm:^1.1.2"
|
||||
"@protobufjs/codegen": "npm:^2.0.4"
|
||||
"@protobufjs/eventemitter": "npm:^1.1.0"
|
||||
"@protobufjs/fetch": "npm:^1.1.0"
|
||||
"@protobufjs/float": "npm:^1.0.2"
|
||||
"@protobufjs/inquire": "npm:^1.1.0"
|
||||
"@protobufjs/path": "npm:^1.1.2"
|
||||
"@protobufjs/pool": "npm:^1.1.0"
|
||||
"@protobufjs/utf8": "npm:^1.1.0"
|
||||
"@types/node": "npm:>=13.7.0"
|
||||
long: "npm:^5.0.0"
|
||||
checksum: 10c0/c4ac6298280dfe6116e7a1b722310de807037b1abed816db2af2a595ddc9dc6160447eebd4ad8e2968d41f0f85375f62e893adfe760af1a943d195931c7bb875
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proxy-addr@npm:^2.0.7":
|
||||
version: 2.0.7
|
||||
resolution: "proxy-addr@npm:2.0.7"
|
||||
@ -16465,6 +16907,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pure-color@npm:^1.2.0":
|
||||
version: 1.3.0
|
||||
resolution: "pure-color@npm:1.3.0"
|
||||
checksum: 10c0/50d0e088ad0349bdd508cddf7c7afbb2d14ba3c047628dbfcfddf467a98f10462caf91f3227172ada88f64afaf761c499ecba0d4053b06926f0f914769be24b9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs@npm:^6.14.0":
|
||||
version: 6.14.0
|
||||
resolution: "qs@npm:6.14.0"
|
||||
@ -17076,6 +17525,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-base16-styling@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "react-base16-styling@npm:0.6.0"
|
||||
dependencies:
|
||||
base16: "npm:^1.0.0"
|
||||
lodash.curry: "npm:^4.0.1"
|
||||
lodash.flow: "npm:^3.3.0"
|
||||
pure-color: "npm:^1.2.0"
|
||||
checksum: 10c0/4887ac57b36fedc7e1ebc99ae431c5feb07d60a9150770d0ca3a59f4ae7059434ea8813ca4f915e7434d4d8d8529b9ba072ceb85041fd52ca1cd6289c57c9621
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dom@npm:^19.0.0":
|
||||
version: 19.1.0
|
||||
resolution: "react-dom@npm:19.1.0"
|
||||
@ -17147,6 +17608,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-json-view@npm:^1.21.3":
|
||||
version: 1.21.3
|
||||
resolution: "react-json-view@npm:1.21.3"
|
||||
dependencies:
|
||||
flux: "npm:^4.0.1"
|
||||
react-base16-styling: "npm:^0.6.0"
|
||||
react-lifecycles-compat: "npm:^3.0.4"
|
||||
react-textarea-autosize: "npm:^8.3.2"
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^16.3.0 || ^15.5.4
|
||||
react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4
|
||||
checksum: 10c0/f41b38e599f148cf922f60390e56bb821f17a091373b08310fd82ebc526428683011751aa023687041481a46b20aeb1c47f660979d43db77674486aec9dc1d3f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-lifecycles-compat@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "react-lifecycles-compat@npm:3.0.4"
|
||||
checksum: 10c0/1d0df3c85af79df720524780f00c064d53a9dd1899d785eddb7264b378026979acbddb58a4b7e06e7d0d12aa1494fd5754562ee55d32907b15601068dae82c27
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-markdown@npm:^10.1.0":
|
||||
version: 10.1.0
|
||||
resolution: "react-markdown@npm:10.1.0"
|
||||
@ -17254,6 +17737,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-textarea-autosize@npm:^8.3.2":
|
||||
version: 8.5.9
|
||||
resolution: "react-textarea-autosize@npm:8.5.9"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.20.13"
|
||||
use-composed-ref: "npm:^1.3.0"
|
||||
use-latest: "npm:^1.2.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/3a924db59259a6e3b834dcddc12a8661b43dcdaa1b43c41c732e0548bb2761e9a53dbc6db4e0d9e237728b4869e42c25e5417408b0391aec290c90874733a09f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-window@npm:^1.8.11":
|
||||
version: 1.8.11
|
||||
resolution: "react-window@npm:1.8.11"
|
||||
@ -17393,6 +17889,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reflect-metadata@npm:0.2.2":
|
||||
version: 0.2.2
|
||||
resolution: "reflect-metadata@npm:0.2.2"
|
||||
checksum: 10c0/1cd93a15ea291e420204955544637c264c216e7aac527470e393d54b4bb075f10a17e60d8168ec96600c7e0b9fcc0cb0bb6e91c3fbf5b0d8c9056f04e6ac1ec2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"regex-recursion@npm:^6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "regex-recursion@npm:6.0.2"
|
||||
@ -19361,6 +19864,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ua-parser-js@npm:^1.0.35":
|
||||
version: 1.0.40
|
||||
resolution: "ua-parser-js@npm:1.0.40"
|
||||
bin:
|
||||
ua-parser-js: script/cli.js
|
||||
checksum: 10c0/2b6ac642c74323957dae142c31f72287f2420c12dced9603d989b96c132b80232779c429b296d7de4012ef8b64e0d8fadc53c639ef06633ce13d785a78b5be6c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "uc.micro@npm:2.1.0"
|
||||
@ -19653,6 +20165,44 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-composed-ref@npm:^1.3.0":
|
||||
version: 1.4.0
|
||||
resolution: "use-composed-ref@npm:1.4.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/c77e0cba9579b7746d52feaf3ce77d8c345f266c9c1ef46584ae68f54646537c87b2ad97f5219a4b1db52f97ec2905e88e5b146add1f28f7e457bd52ca1b93cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-isomorphic-layout-effect@npm:^1.1.1":
|
||||
version: 1.2.1
|
||||
resolution: "use-isomorphic-layout-effect@npm:1.2.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/4d3c1124d630fbe09c1d2a16af0cd78ac2fe1d22eb24a178363e3d84a897659cc04e8e8cd71f66ff78ff75ef8287fa72e746cb213b96c1097e70e4b4ed69f63f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-latest@npm:^1.2.1":
|
||||
version: 1.3.0
|
||||
resolution: "use-latest@npm:1.3.0"
|
||||
dependencies:
|
||||
use-isomorphic-layout-effect: "npm:^1.1.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/067c648814ad0c1f1e89d2d0e496254b05c4bed6a34e23045b4413824222aab08fd803c59a42852acc16830c17567d03f8c90af0a62be2f4e4b931454d079798
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-memo-one@npm:^1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "use-memo-one@npm:1.1.3"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user