bingocode 1.0.41 → 1.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bingo-win.cjs +2 -1
- package/bin/bingocode-win.cjs +2 -1
- package/bin/claude-win.cjs +2 -1
- package/bun.lock +1716 -0
- package/package.json +14 -2
- package/src/server/config/providers.yaml +1 -1
- package/src/server/proxy/transform/anthropicToOpenaiChat.ts +11 -4
- package/adapters/README.md +0 -87
- package/adapters/common/__tests__/chat-queue.test.ts +0 -61
- package/adapters/common/__tests__/format.test.ts +0 -148
- package/adapters/common/__tests__/http-client.test.ts +0 -105
- package/adapters/common/__tests__/message-buffer.test.ts +0 -84
- package/adapters/common/__tests__/message-dedup.test.ts +0 -57
- package/adapters/common/__tests__/session-store.test.ts +0 -62
- package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
- package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
- package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
- package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
- package/adapters/common/attachment/attachment-limits.ts +0 -58
- package/adapters/common/attachment/attachment-store.ts +0 -121
- package/adapters/common/attachment/attachment-types.ts +0 -29
- package/adapters/common/attachment/image-block-watcher.ts +0 -94
- package/adapters/common/chat-queue.ts +0 -24
- package/adapters/common/config.ts +0 -96
- package/adapters/common/format.ts +0 -229
- package/adapters/common/http-client.ts +0 -107
- package/adapters/common/message-buffer.ts +0 -91
- package/adapters/common/message-dedup.ts +0 -57
- package/adapters/common/pairing.ts +0 -149
- package/adapters/common/session-store.ts +0 -60
- package/adapters/common/ws-bridge.ts +0 -282
- package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
- package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
- package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
- package/adapters/feishu/__tests__/feishu.test.ts +0 -907
- package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
- package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
- package/adapters/feishu/__tests__/media.test.ts +0 -120
- package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
- package/adapters/feishu/card-errors.ts +0 -151
- package/adapters/feishu/cardkit.ts +0 -294
- package/adapters/feishu/extract-payload.ts +0 -95
- package/adapters/feishu/flush-controller.ts +0 -149
- package/adapters/feishu/index.ts +0 -1275
- package/adapters/feishu/markdown-style.ts +0 -212
- package/adapters/feishu/media.ts +0 -176
- package/adapters/feishu/streaming-card.ts +0 -612
- package/adapters/package.json +0 -23
- package/adapters/telegram/__tests__/media.test.ts +0 -86
- package/adapters/telegram/__tests__/telegram.test.ts +0 -115
- package/adapters/telegram/index.ts +0 -754
- package/adapters/telegram/media.ts +0 -89
- package/adapters/tsconfig.json +0 -18
- package/runtime/mac_helper.py +0 -775
- package/runtime/requirements-win.txt +0 -7
- package/runtime/requirements.txt +0 -6
- package/runtime/test_helpers.py +0 -322
- package/runtime/win_helper.py +0 -723
- package/scripts/count-app-loc.ts +0 -256
- package/scripts/release.ts +0 -130
- package/start-cli.bat +0 -7
- package/stubs/ant-claude-for-chrome-mcp.ts +0 -24
- package/stubs/color-diff-napi.ts +0 -45
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure, stateful extractor that watches a stream of assistant text for
|
|
3
|
-
* markdown image references (``) and emits PendingUpload
|
|
4
|
-
* records. Used by IM adapters to know which images to upload to IM.
|
|
5
|
-
*
|
|
6
|
-
* - Buffers input so an image marker split across multiple feed() calls
|
|
7
|
-
* still gets detected.
|
|
8
|
-
* - Dedups by fingerprint of the source so the same image is only emitted
|
|
9
|
-
* once per watcher lifetime.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { PendingUpload } from './attachment-types.js'
|
|
13
|
-
|
|
14
|
-
// Matches a complete markdown image: 
|
|
15
|
-
// `alt` may be empty; `target` stops at the first closing paren.
|
|
16
|
-
const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g
|
|
17
|
-
|
|
18
|
-
function fingerprint(raw: string): string {
|
|
19
|
-
let h = 5381
|
|
20
|
-
for (let i = 0; i < raw.length; i++) {
|
|
21
|
-
h = ((h << 5) + h) ^ raw.charCodeAt(i)
|
|
22
|
-
}
|
|
23
|
-
return (h >>> 0).toString(16)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function classify(target: string): PendingUpload['source'] | null {
|
|
27
|
-
if (target.startsWith('data:')) {
|
|
28
|
-
const m = /^data:([^;,]+);base64,(.+)$/.exec(target)
|
|
29
|
-
if (!m) return null
|
|
30
|
-
return { kind: 'base64', mime: m[1]!, data: m[2]! }
|
|
31
|
-
}
|
|
32
|
-
if (target.startsWith('file://')) {
|
|
33
|
-
return { kind: 'path', path: target.slice('file://'.length) }
|
|
34
|
-
}
|
|
35
|
-
if (target.startsWith('http://') || target.startsWith('https://')) {
|
|
36
|
-
return { kind: 'url', url: target }
|
|
37
|
-
}
|
|
38
|
-
if (target.startsWith('/')) {
|
|
39
|
-
return { kind: 'path', path: target }
|
|
40
|
-
}
|
|
41
|
-
return null // relative paths — skip, we can't resolve them safely
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class ImageBlockWatcher {
|
|
45
|
-
private buffer = ''
|
|
46
|
-
private seen = new Set<string>()
|
|
47
|
-
private accumulated: PendingUpload[] = []
|
|
48
|
-
|
|
49
|
-
/** Feed a new chunk of streaming text; returns any NEW PendingUploads. */
|
|
50
|
-
feed(chunk: string): PendingUpload[] {
|
|
51
|
-
this.buffer += chunk
|
|
52
|
-
const out: PendingUpload[] = []
|
|
53
|
-
|
|
54
|
-
IMAGE_RE.lastIndex = 0
|
|
55
|
-
let lastConsumedEnd = 0
|
|
56
|
-
let m: RegExpExecArray | null
|
|
57
|
-
while ((m = IMAGE_RE.exec(this.buffer)) !== null) {
|
|
58
|
-
const [, alt, target] = m
|
|
59
|
-
const source = classify(target!)
|
|
60
|
-
if (source) {
|
|
61
|
-
const id = fingerprint(`${source.kind}:${target}`)
|
|
62
|
-
if (!this.seen.has(id)) {
|
|
63
|
-
this.seen.add(id)
|
|
64
|
-
const pending: PendingUpload = { id, source, alt: alt || undefined }
|
|
65
|
-
out.push(pending)
|
|
66
|
-
this.accumulated.push(pending)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
lastConsumedEnd = m.index + m[0].length
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Preserve tail that might contain a partially-received marker.
|
|
73
|
-
if (lastConsumedEnd > 0) {
|
|
74
|
-
this.buffer = this.buffer.slice(lastConsumedEnd)
|
|
75
|
-
}
|
|
76
|
-
if (this.buffer.length > 4096) {
|
|
77
|
-
this.buffer = this.buffer.slice(-2048)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return out
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Return everything seen so far (for end-of-stream reconciliation). */
|
|
84
|
-
drain(): PendingUpload[] {
|
|
85
|
-
return [...this.accumulated]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Reset watcher state (use at /clear or new session). */
|
|
89
|
-
reset(): void {
|
|
90
|
-
this.buffer = ''
|
|
91
|
-
this.seen.clear()
|
|
92
|
-
this.accumulated = []
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话串行队列
|
|
3
|
-
*
|
|
4
|
-
* 同一 chatId 的消息串行处理,防并发冲突。
|
|
5
|
-
* 不同 chatId 之间互不影响。
|
|
6
|
-
* 参考 openclaw-lark chat-queue.ts 的 Promise 链设计。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const queues = new Map<string, Promise<void>>()
|
|
10
|
-
|
|
11
|
-
export async function enqueue(chatId: string, fn: () => Promise<void>): Promise<void> {
|
|
12
|
-
const prev = queues.get(chatId) ?? Promise.resolve()
|
|
13
|
-
const next = prev.then(fn, () => fn()).catch((err) => {
|
|
14
|
-
console.error(`[ChatQueue] Error in task for chat ${chatId}:`, err)
|
|
15
|
-
})
|
|
16
|
-
queues.set(chatId, next)
|
|
17
|
-
// Clean up after completion to avoid memory leak for one-off chats
|
|
18
|
-
next.finally(() => {
|
|
19
|
-
if (queues.get(chatId) === next) {
|
|
20
|
-
queues.delete(chatId)
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
return next
|
|
24
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter 配置加载
|
|
3
|
-
*
|
|
4
|
-
* 优先级:环境变量 > ~/.claude/adapters.json > 默认值
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as fs from 'node:fs'
|
|
8
|
-
import * as os from 'node:os'
|
|
9
|
-
import * as path from 'node:path'
|
|
10
|
-
|
|
11
|
-
export type PairedUser = {
|
|
12
|
-
userId: string | number
|
|
13
|
-
displayName: string
|
|
14
|
-
pairedAt: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type PairingState = {
|
|
18
|
-
code: string | null
|
|
19
|
-
expiresAt: number | null
|
|
20
|
-
createdAt: number | null
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type TelegramConfig = {
|
|
24
|
-
botToken: string
|
|
25
|
-
allowedUsers: number[]
|
|
26
|
-
pairedUsers: PairedUser[]
|
|
27
|
-
defaultWorkDir: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type FeishuConfig = {
|
|
31
|
-
appId: string
|
|
32
|
-
appSecret: string
|
|
33
|
-
encryptKey: string
|
|
34
|
-
verificationToken: string
|
|
35
|
-
allowedUsers: string[]
|
|
36
|
-
pairedUsers: PairedUser[]
|
|
37
|
-
defaultWorkDir: string
|
|
38
|
-
streamingCard: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type AdapterConfig = {
|
|
42
|
-
serverUrl: string
|
|
43
|
-
defaultProjectDir: string
|
|
44
|
-
pairing: PairingState
|
|
45
|
-
telegram: TelegramConfig
|
|
46
|
-
feishu: FeishuConfig
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getConfigPath(): string {
|
|
50
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
51
|
-
return path.join(configDir, 'adapters.json')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function loadFile(): Record<string, any> {
|
|
55
|
-
try {
|
|
56
|
-
return JSON.parse(fs.readFileSync(getConfigPath(), 'utf-8'))
|
|
57
|
-
} catch (err: any) {
|
|
58
|
-
if (err?.code !== 'ENOENT') {
|
|
59
|
-
console.warn(`[Config] Failed to parse ${getConfigPath()}, using defaults`)
|
|
60
|
-
}
|
|
61
|
-
return {}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function loadConfig(): AdapterConfig {
|
|
66
|
-
const file = loadFile()
|
|
67
|
-
const tg = file.telegram ?? {}
|
|
68
|
-
const fs_ = file.feishu ?? {}
|
|
69
|
-
const pairing = file.pairing ?? {}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
serverUrl: process.env.ADAPTER_SERVER_URL || file.serverUrl || 'ws://127.0.0.1:3456',
|
|
73
|
-
defaultProjectDir: file.defaultProjectDir || '',
|
|
74
|
-
pairing: {
|
|
75
|
-
code: pairing.code ?? null,
|
|
76
|
-
expiresAt: pairing.expiresAt ?? null,
|
|
77
|
-
createdAt: pairing.createdAt ?? null,
|
|
78
|
-
},
|
|
79
|
-
telegram: {
|
|
80
|
-
botToken: process.env.TELEGRAM_BOT_TOKEN || tg.botToken || '',
|
|
81
|
-
allowedUsers: tg.allowedUsers ?? [],
|
|
82
|
-
pairedUsers: tg.pairedUsers ?? [],
|
|
83
|
-
defaultWorkDir: tg.defaultWorkDir || process.cwd(),
|
|
84
|
-
},
|
|
85
|
-
feishu: {
|
|
86
|
-
appId: process.env.FEISHU_APP_ID || fs_.appId || '',
|
|
87
|
-
appSecret: process.env.FEISHU_APP_SECRET || fs_.appSecret || '',
|
|
88
|
-
encryptKey: process.env.FEISHU_ENCRYPT_KEY || fs_.encryptKey || '',
|
|
89
|
-
verificationToken: process.env.FEISHU_VERIFICATION_TOKEN || fs_.verificationToken || '',
|
|
90
|
-
allowedUsers: fs_.allowedUsers ?? [],
|
|
91
|
-
pairedUsers: fs_.pairedUsers ?? [],
|
|
92
|
-
defaultWorkDir: fs_.defaultWorkDir || process.cwd(),
|
|
93
|
-
streamingCard: fs_.streamingCard ?? false,
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 消息格式化工具
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
type AdapterChatState =
|
|
6
|
-
| 'idle'
|
|
7
|
-
| 'thinking'
|
|
8
|
-
| 'streaming'
|
|
9
|
-
| 'tool_executing'
|
|
10
|
-
| 'permission_pending'
|
|
11
|
-
|
|
12
|
-
type ImStatusSummary = {
|
|
13
|
-
sessionId?: string
|
|
14
|
-
projectName?: string | null
|
|
15
|
-
branch?: string | null
|
|
16
|
-
model?: string | null
|
|
17
|
-
state?: AdapterChatState | null
|
|
18
|
-
verb?: string | null
|
|
19
|
-
pendingPermissionCount?: number
|
|
20
|
-
taskCounts?: {
|
|
21
|
-
total: number
|
|
22
|
-
pending: number
|
|
23
|
-
inProgress: number
|
|
24
|
-
completed: number
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const IM_HELP_LINES = [
|
|
29
|
-
'/new [项目] — 新建会话或切换项目',
|
|
30
|
-
'/projects — 查看最近项目',
|
|
31
|
-
'/status — 查看当前会话状态',
|
|
32
|
-
'/clear — 清空当前会话上下文',
|
|
33
|
-
'/stop — 停止当前生成',
|
|
34
|
-
'/help — 显示这份帮助',
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
/** Split text into chunks that fit within a character limit, respecting paragraph/sentence boundaries. */
|
|
38
|
-
export function splitMessage(text: string, limit: number): string[] {
|
|
39
|
-
if (text.length <= limit) return [text]
|
|
40
|
-
|
|
41
|
-
const chunks: string[] = []
|
|
42
|
-
let remaining = text
|
|
43
|
-
|
|
44
|
-
while (remaining.length > 0) {
|
|
45
|
-
if (remaining.length <= limit) {
|
|
46
|
-
chunks.push(remaining)
|
|
47
|
-
break
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let splitAt = remaining.lastIndexOf('\n\n', limit)
|
|
51
|
-
if (splitAt <= 0) splitAt = remaining.lastIndexOf('\n', limit)
|
|
52
|
-
if (splitAt <= 0) splitAt = remaining.lastIndexOf('. ', limit)
|
|
53
|
-
if (splitAt <= 0) splitAt = remaining.lastIndexOf(' ', limit)
|
|
54
|
-
if (splitAt <= 0) splitAt = limit
|
|
55
|
-
|
|
56
|
-
// Include the delimiter for paragraph/sentence breaks
|
|
57
|
-
if (remaining[splitAt] === '\n' || remaining[splitAt] === '.') splitAt += 1
|
|
58
|
-
|
|
59
|
-
chunks.push(remaining.slice(0, splitAt).trimEnd())
|
|
60
|
-
remaining = remaining.slice(splitAt).trimStart()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return chunks
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Format tool use info for display in IM. */
|
|
67
|
-
export function formatToolUse(toolName: string, input: unknown): string {
|
|
68
|
-
const inp = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>
|
|
69
|
-
const summary = formatToolSummary(toolName, inp)
|
|
70
|
-
if (summary) return `🔧 ${toolName} ${summary}`
|
|
71
|
-
const preview = truncateInput(input, 200)
|
|
72
|
-
return `🔧 ${toolName}\n${preview}`
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Generate a concise human-readable summary for common tools. */
|
|
76
|
-
function formatToolSummary(tool: string, inp: Record<string, unknown>): string | null {
|
|
77
|
-
switch (tool) {
|
|
78
|
-
case 'Bash': {
|
|
79
|
-
const desc = inp.description as string | undefined
|
|
80
|
-
const cmd = inp.command as string | undefined
|
|
81
|
-
if (desc) return desc
|
|
82
|
-
if (cmd) return truncate(cmd, 120)
|
|
83
|
-
return null
|
|
84
|
-
}
|
|
85
|
-
case 'Read': {
|
|
86
|
-
const fp = inp.file_path as string | undefined
|
|
87
|
-
if (fp) return shortPath(fp)
|
|
88
|
-
return null
|
|
89
|
-
}
|
|
90
|
-
case 'Edit': {
|
|
91
|
-
const fp = inp.file_path as string | undefined
|
|
92
|
-
if (fp) return shortPath(fp)
|
|
93
|
-
return null
|
|
94
|
-
}
|
|
95
|
-
case 'Write': {
|
|
96
|
-
const fp = inp.file_path as string | undefined
|
|
97
|
-
if (fp) return shortPath(fp)
|
|
98
|
-
return null
|
|
99
|
-
}
|
|
100
|
-
case 'Grep': {
|
|
101
|
-
const pat = inp.pattern as string | undefined
|
|
102
|
-
const p = inp.path as string | undefined
|
|
103
|
-
if (pat) return `"${truncate(pat, 60)}"` + (p ? ` in ${shortPath(p)}` : '')
|
|
104
|
-
return null
|
|
105
|
-
}
|
|
106
|
-
case 'Glob': {
|
|
107
|
-
const pat = inp.pattern as string | undefined
|
|
108
|
-
return pat ? `"${pat}"` : null
|
|
109
|
-
}
|
|
110
|
-
case 'Skill': {
|
|
111
|
-
const skill = inp.skill as string | undefined
|
|
112
|
-
return skill || null
|
|
113
|
-
}
|
|
114
|
-
case 'Agent': {
|
|
115
|
-
const desc = inp.description as string | undefined
|
|
116
|
-
return desc || null
|
|
117
|
-
}
|
|
118
|
-
case 'WebFetch': {
|
|
119
|
-
const url = inp.url as string | undefined
|
|
120
|
-
return url ? truncate(url, 120) : null
|
|
121
|
-
}
|
|
122
|
-
case 'WebSearch': {
|
|
123
|
-
const q = inp.query as string | undefined
|
|
124
|
-
return q ? `"${truncate(q, 80)}"` : null
|
|
125
|
-
}
|
|
126
|
-
default:
|
|
127
|
-
return null
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function shortPath(fp: string): string {
|
|
132
|
-
const parts = fp.split('/')
|
|
133
|
-
return parts.length > 3 ? '…/' + parts.slice(-3).join('/') : fp
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function truncate(s: string, max: number): string {
|
|
137
|
-
return s.length > max ? s.slice(0, max) + '…' : s
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Format a permission request for display in IM. */
|
|
141
|
-
export function formatPermissionRequest(toolName: string, input: unknown, requestId: string): string {
|
|
142
|
-
const preview = truncateInput(input, 300)
|
|
143
|
-
return `🔐 需要权限确认 [${requestId}]\n工具: ${toolName}\n${preview}`
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Truncate tool input to a preview string. */
|
|
147
|
-
export function truncateInput(input: unknown, maxLen: number): string {
|
|
148
|
-
try {
|
|
149
|
-
const s = typeof input === 'string' ? input : JSON.stringify(input, null, 2)
|
|
150
|
-
return s.length > maxLen ? s.slice(0, maxLen) + '…' : s
|
|
151
|
-
} catch {
|
|
152
|
-
return '(unserializable)'
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Escape special characters for Telegram MarkdownV2. */
|
|
157
|
-
export function escapeMarkdownV2(text: string): string {
|
|
158
|
-
return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, '\\$1')
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function formatImHelp(): string {
|
|
162
|
-
return `可用命令:\n\n${IM_HELP_LINES.join('\n')}`
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function formatImStatus(summary: ImStatusSummary | null): string {
|
|
166
|
-
if (!summary?.sessionId) {
|
|
167
|
-
return '当前没有活动会话。\n\n发送 /new 新建会话,或发送 /projects 选择项目。'
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const lines = ['当前会话状态:']
|
|
171
|
-
|
|
172
|
-
if (summary.projectName) {
|
|
173
|
-
lines.push(`项目: ${summary.projectName}${summary.branch ? ` (${summary.branch})` : ''}`)
|
|
174
|
-
} else if (summary.branch) {
|
|
175
|
-
lines.push(`分支: ${summary.branch}`)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
lines.push(`会话: ${shortSessionId(summary.sessionId)}`)
|
|
179
|
-
|
|
180
|
-
if (summary.model) {
|
|
181
|
-
lines.push(`模型: ${summary.model}`)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
lines.push(`状态: ${formatAdapterChatState(summary.state, summary.verb)}`)
|
|
185
|
-
|
|
186
|
-
const pendingPermissionCount = summary.pendingPermissionCount ?? 0
|
|
187
|
-
if (pendingPermissionCount > 0) {
|
|
188
|
-
lines.push(`审批: ${pendingPermissionCount} 个待确认`)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const taskCounts = summary.taskCounts
|
|
192
|
-
if (taskCounts && taskCounts.total > 0) {
|
|
193
|
-
const taskParts = [`总计 ${taskCounts.total}`]
|
|
194
|
-
if (taskCounts.inProgress > 0) taskParts.push(`进行中 ${taskCounts.inProgress}`)
|
|
195
|
-
if (taskCounts.pending > 0) taskParts.push(`待处理 ${taskCounts.pending}`)
|
|
196
|
-
if (taskCounts.completed > 0) taskParts.push(`已完成 ${taskCounts.completed}`)
|
|
197
|
-
lines.push(`任务: ${taskParts.join(' · ')}`)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return lines.join('\n')
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function formatAdapterChatState(
|
|
204
|
-
state: AdapterChatState | null | undefined,
|
|
205
|
-
verb: string | null | undefined,
|
|
206
|
-
): string {
|
|
207
|
-
const label = (() => {
|
|
208
|
-
switch (state) {
|
|
209
|
-
case 'thinking':
|
|
210
|
-
return '思考中'
|
|
211
|
-
case 'streaming':
|
|
212
|
-
return '生成中'
|
|
213
|
-
case 'tool_executing':
|
|
214
|
-
return '执行工具中'
|
|
215
|
-
case 'permission_pending':
|
|
216
|
-
return '等待权限确认'
|
|
217
|
-
case 'idle':
|
|
218
|
-
default:
|
|
219
|
-
return '空闲'
|
|
220
|
-
}
|
|
221
|
-
})()
|
|
222
|
-
|
|
223
|
-
if (!verb || verb === 'Thinking') return label
|
|
224
|
-
return `${label} (${verb})`
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function shortSessionId(sessionId: string): string {
|
|
228
|
-
return sessionId.length > 12 ? `${sessionId.slice(0, 8)}…` : sessionId
|
|
229
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
export type RecentProject = {
|
|
2
|
-
projectPath: string
|
|
3
|
-
realPath: string
|
|
4
|
-
projectName: string
|
|
5
|
-
isGit: boolean
|
|
6
|
-
repoName: string | null
|
|
7
|
-
branch: string | null
|
|
8
|
-
modifiedAt: string
|
|
9
|
-
sessionCount: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type GitInfo = {
|
|
13
|
-
branch: string | null
|
|
14
|
-
repoName: string | null
|
|
15
|
-
workDir: string
|
|
16
|
-
changedFiles: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type SessionTask = {
|
|
20
|
-
id: string
|
|
21
|
-
subject: string
|
|
22
|
-
status: 'pending' | 'in_progress' | 'completed'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class AdapterHttpClient {
|
|
26
|
-
readonly httpBaseUrl: string
|
|
27
|
-
|
|
28
|
-
constructor(wsUrl: string) {
|
|
29
|
-
this.httpBaseUrl = wsUrl
|
|
30
|
-
.replace(/^ws:/, 'http:')
|
|
31
|
-
.replace(/^wss:/, 'https:')
|
|
32
|
-
.replace(/\/$/, '')
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async createSession(workDir: string): Promise<string> {
|
|
36
|
-
const res = await fetch(`${this.httpBaseUrl}/api/sessions`, {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { 'Content-Type': 'application/json' },
|
|
39
|
-
body: JSON.stringify({ workDir }),
|
|
40
|
-
})
|
|
41
|
-
if (!res.ok) {
|
|
42
|
-
const err = await res.json().catch(() => ({ message: res.statusText }))
|
|
43
|
-
throw new Error(`Failed to create session: ${(err as any).message}`)
|
|
44
|
-
}
|
|
45
|
-
const data = (await res.json()) as { sessionId: string }
|
|
46
|
-
return data.sessionId
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async listRecentProjects(): Promise<RecentProject[]> {
|
|
50
|
-
const res = await fetch(`${this.httpBaseUrl}/api/sessions/recent-projects`)
|
|
51
|
-
if (!res.ok) {
|
|
52
|
-
throw new Error(`Failed to list projects: ${res.statusText}`)
|
|
53
|
-
}
|
|
54
|
-
const data = (await res.json()) as { projects: RecentProject[] }
|
|
55
|
-
return data.projects
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Match a project by index (1-based) or fuzzy name from recent projects.
|
|
60
|
-
* Returns { project, ambiguous[] } — ambiguous is set when multiple projects match.
|
|
61
|
-
*/
|
|
62
|
-
async matchProject(query: string): Promise<{ project?: RecentProject; ambiguous?: RecentProject[] }> {
|
|
63
|
-
const projects = await this.listRecentProjects()
|
|
64
|
-
|
|
65
|
-
// Try as 1-based index
|
|
66
|
-
const num = parseInt(query, 10)
|
|
67
|
-
if (!isNaN(num) && num >= 1 && num <= projects.length && String(num) === query.trim()) {
|
|
68
|
-
return { project: projects[num - 1] }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const q = query.toLowerCase()
|
|
72
|
-
|
|
73
|
-
// Exact project name match
|
|
74
|
-
const exact = projects.find(p => p.projectName.toLowerCase() === q)
|
|
75
|
-
if (exact) return { project: exact }
|
|
76
|
-
|
|
77
|
-
// Fuzzy: name or path contains query
|
|
78
|
-
const matches = projects.filter(p =>
|
|
79
|
-
p.projectName.toLowerCase().includes(q) ||
|
|
80
|
-
p.realPath.toLowerCase().includes(q)
|
|
81
|
-
)
|
|
82
|
-
if (matches.length === 1) return { project: matches[0] }
|
|
83
|
-
if (matches.length > 1) return { ambiguous: matches }
|
|
84
|
-
|
|
85
|
-
return {}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async getGitInfo(sessionId: string): Promise<GitInfo> {
|
|
89
|
-
const res = await fetch(`${this.httpBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/git-info`)
|
|
90
|
-
if (!res.ok) {
|
|
91
|
-
const err = await res.json().catch(() => ({ message: res.statusText }))
|
|
92
|
-
throw new Error(`Failed to load git info: ${(err as any).message}`)
|
|
93
|
-
}
|
|
94
|
-
return (await res.json()) as GitInfo
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async getTasksForSession(sessionId: string): Promise<SessionTask[]> {
|
|
98
|
-
const res = await fetch(`${this.httpBaseUrl}/api/tasks/lists/${encodeURIComponent(sessionId)}`)
|
|
99
|
-
if (!res.ok) {
|
|
100
|
-
if (res.status === 404) return []
|
|
101
|
-
const err = await res.json().catch(() => ({ message: res.statusText }))
|
|
102
|
-
throw new Error(`Failed to load tasks: ${(err as any).message}`)
|
|
103
|
-
}
|
|
104
|
-
const data = (await res.json()) as { tasks?: SessionTask[] }
|
|
105
|
-
return Array.isArray(data.tasks) ? data.tasks : []
|
|
106
|
-
}
|
|
107
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 流式消息缓冲
|
|
3
|
-
*
|
|
4
|
-
* 将 content_delta 累积后按时间窗口或字符数批量 flush。
|
|
5
|
-
* 用于 Telegram editMessage / 飞书流式卡片更新。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export type FlushCallback = (text: string, isComplete: boolean) => void | Promise<void>
|
|
9
|
-
|
|
10
|
-
const DEFAULT_INTERVAL_MS = 500
|
|
11
|
-
const DEFAULT_CHAR_THRESHOLD = 200
|
|
12
|
-
|
|
13
|
-
export class MessageBuffer {
|
|
14
|
-
private buffer = ''
|
|
15
|
-
private timer: ReturnType<typeof setTimeout> | null = null
|
|
16
|
-
private flushing = false
|
|
17
|
-
private pendingComplete = false
|
|
18
|
-
|
|
19
|
-
constructor(
|
|
20
|
-
private onFlush: FlushCallback,
|
|
21
|
-
private intervalMs = DEFAULT_INTERVAL_MS,
|
|
22
|
-
private charThreshold = DEFAULT_CHAR_THRESHOLD,
|
|
23
|
-
) {}
|
|
24
|
-
|
|
25
|
-
/** Append text delta. Triggers flush if threshold reached. */
|
|
26
|
-
append(text: string): void {
|
|
27
|
-
this.buffer += text
|
|
28
|
-
if (this.buffer.length >= this.charThreshold) {
|
|
29
|
-
this.scheduleFlush()
|
|
30
|
-
} else if (!this.timer) {
|
|
31
|
-
this.timer = setTimeout(() => this.flush(false), this.intervalMs)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Immediately flush all remaining content (called on message_complete). */
|
|
36
|
-
async complete(): Promise<void> {
|
|
37
|
-
if (this.timer) {
|
|
38
|
-
clearTimeout(this.timer)
|
|
39
|
-
this.timer = null
|
|
40
|
-
}
|
|
41
|
-
if (this.flushing) {
|
|
42
|
-
// A flush is in-flight; mark pending so it fires after current flush finishes
|
|
43
|
-
this.pendingComplete = true
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
await this.flush(true)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Reset the buffer for a new message. */
|
|
50
|
-
reset(): void {
|
|
51
|
-
this.buffer = ''
|
|
52
|
-
this.pendingComplete = false
|
|
53
|
-
if (this.timer) {
|
|
54
|
-
clearTimeout(this.timer)
|
|
55
|
-
this.timer = null
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private scheduleFlush(): void {
|
|
60
|
-
if (this.timer) {
|
|
61
|
-
clearTimeout(this.timer)
|
|
62
|
-
this.timer = null
|
|
63
|
-
}
|
|
64
|
-
queueMicrotask(() => this.flush(false))
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private async flush(isComplete: boolean): Promise<void> {
|
|
68
|
-
if (this.timer) {
|
|
69
|
-
clearTimeout(this.timer)
|
|
70
|
-
this.timer = null
|
|
71
|
-
}
|
|
72
|
-
if (this.flushing) return
|
|
73
|
-
if (this.buffer.length === 0) return
|
|
74
|
-
|
|
75
|
-
this.flushing = true
|
|
76
|
-
const text = this.buffer
|
|
77
|
-
this.buffer = ''
|
|
78
|
-
try {
|
|
79
|
-
await this.onFlush(text, isComplete)
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.error('[MessageBuffer] Flush error:', err)
|
|
82
|
-
} finally {
|
|
83
|
-
this.flushing = false
|
|
84
|
-
// If complete() was called while we were flushing, do the final flush now
|
|
85
|
-
if (this.pendingComplete) {
|
|
86
|
-
this.pendingComplete = false
|
|
87
|
-
await this.flush(true)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 消息去重
|
|
3
|
-
*
|
|
4
|
-
* 防止 WebSocket 重连等场景下消息重复处理。
|
|
5
|
-
* 参考 openclaw-lark dedup.ts 的 Map + TTL + 容量 设计。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const DEFAULT_TTL_MS = 10 * 60_000 // 10 minutes
|
|
9
|
-
const DEFAULT_MAX_ENTRIES = 5000
|
|
10
|
-
const SWEEP_INTERVAL_MS = 60_000 // 1 minute
|
|
11
|
-
|
|
12
|
-
export class MessageDedup {
|
|
13
|
-
private store = new Map<string, number>()
|
|
14
|
-
private sweepTimer: ReturnType<typeof setInterval>
|
|
15
|
-
|
|
16
|
-
constructor(
|
|
17
|
-
private ttlMs = DEFAULT_TTL_MS,
|
|
18
|
-
private maxEntries = DEFAULT_MAX_ENTRIES,
|
|
19
|
-
) {
|
|
20
|
-
this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Returns true if this is a NEW message, false if duplicate. */
|
|
24
|
-
tryRecord(id: string): boolean {
|
|
25
|
-
const now = Date.now()
|
|
26
|
-
const existing = this.store.get(id)
|
|
27
|
-
|
|
28
|
-
if (existing !== undefined && now - existing < this.ttlMs) {
|
|
29
|
-
return false // duplicate
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Evict oldest if at capacity
|
|
33
|
-
if (this.store.size >= this.maxEntries) {
|
|
34
|
-
const oldest = this.store.keys().next().value
|
|
35
|
-
if (oldest !== undefined) this.store.delete(oldest)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
this.store.set(id, now)
|
|
39
|
-
return true
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private sweep(): void {
|
|
43
|
-
const now = Date.now()
|
|
44
|
-
for (const [key, ts] of this.store) {
|
|
45
|
-
if (now - ts >= this.ttlMs) {
|
|
46
|
-
this.store.delete(key)
|
|
47
|
-
} else {
|
|
48
|
-
break // Map preserves insertion order; once fresh, rest is fresh
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
destroy(): void {
|
|
54
|
-
clearInterval(this.sweepTimer)
|
|
55
|
-
this.store.clear()
|
|
56
|
-
}
|
|
57
|
-
}
|