bingocode 1.0.40 → 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.
Files changed (63) hide show
  1. package/bin/bingo-win.cjs +2 -1
  2. package/bin/bingocode-win.cjs +2 -1
  3. package/bin/claude-win.cjs +2 -1
  4. package/bun.lock +1716 -0
  5. package/package.json +14 -2
  6. package/src/server/config/providers.yaml +1 -1
  7. package/src/server/proxy/transform/anthropicToOpenaiChat.ts +23 -9
  8. package/adapters/README.md +0 -87
  9. package/adapters/common/__tests__/chat-queue.test.ts +0 -61
  10. package/adapters/common/__tests__/format.test.ts +0 -148
  11. package/adapters/common/__tests__/http-client.test.ts +0 -105
  12. package/adapters/common/__tests__/message-buffer.test.ts +0 -84
  13. package/adapters/common/__tests__/message-dedup.test.ts +0 -57
  14. package/adapters/common/__tests__/session-store.test.ts +0 -62
  15. package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
  16. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
  17. package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
  18. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
  19. package/adapters/common/attachment/attachment-limits.ts +0 -58
  20. package/adapters/common/attachment/attachment-store.ts +0 -121
  21. package/adapters/common/attachment/attachment-types.ts +0 -29
  22. package/adapters/common/attachment/image-block-watcher.ts +0 -94
  23. package/adapters/common/chat-queue.ts +0 -24
  24. package/adapters/common/config.ts +0 -96
  25. package/adapters/common/format.ts +0 -229
  26. package/adapters/common/http-client.ts +0 -107
  27. package/adapters/common/message-buffer.ts +0 -91
  28. package/adapters/common/message-dedup.ts +0 -57
  29. package/adapters/common/pairing.ts +0 -149
  30. package/adapters/common/session-store.ts +0 -60
  31. package/adapters/common/ws-bridge.ts +0 -282
  32. package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
  33. package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
  34. package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
  35. package/adapters/feishu/__tests__/feishu.test.ts +0 -907
  36. package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
  37. package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
  38. package/adapters/feishu/__tests__/media.test.ts +0 -120
  39. package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
  40. package/adapters/feishu/card-errors.ts +0 -151
  41. package/adapters/feishu/cardkit.ts +0 -294
  42. package/adapters/feishu/extract-payload.ts +0 -95
  43. package/adapters/feishu/flush-controller.ts +0 -149
  44. package/adapters/feishu/index.ts +0 -1275
  45. package/adapters/feishu/markdown-style.ts +0 -212
  46. package/adapters/feishu/media.ts +0 -176
  47. package/adapters/feishu/streaming-card.ts +0 -612
  48. package/adapters/package.json +0 -23
  49. package/adapters/telegram/__tests__/media.test.ts +0 -86
  50. package/adapters/telegram/__tests__/telegram.test.ts +0 -115
  51. package/adapters/telegram/index.ts +0 -754
  52. package/adapters/telegram/media.ts +0 -89
  53. package/adapters/tsconfig.json +0 -18
  54. package/runtime/mac_helper.py +0 -775
  55. package/runtime/requirements-win.txt +0 -7
  56. package/runtime/requirements.txt +0 -6
  57. package/runtime/test_helpers.py +0 -322
  58. package/runtime/win_helper.py +0 -723
  59. package/scripts/count-app-loc.ts +0 -256
  60. package/scripts/release.ts +0 -130
  61. package/start-cli.bat +0 -7
  62. package/stubs/ant-claude-for-chrome-mcp.ts +0 -24
  63. package/stubs/color-diff-napi.ts +0 -45
@@ -1,121 +0,0 @@
1
- /**
2
- * Local staging directory for IM-downloaded resources.
3
- *
4
- * Layout: {root}/{platform}/{sessionId}/{safeName}
5
- * Default root: ~/.claude/im-downloads
6
- *
7
- * Responsibilities:
8
- * - Generate unique, safe paths from (platform, sessionId, originalName)
9
- * - Atomic write (tmp → rename) so concurrent downloads never corrupt each other
10
- * - GC files that haven't been touched for `retentionMs` (default 24h)
11
- */
12
-
13
- import * as fs from 'node:fs/promises'
14
- import * as fsSync from 'node:fs'
15
- import type { Dirent } from 'node:fs'
16
- import * as path from 'node:path'
17
- import * as os from 'node:os'
18
- import type { ImPlatform } from './attachment-types.js'
19
-
20
- export interface AttachmentStoreConfig {
21
- root: string
22
- retentionMs: number
23
- /** Grace window before a `.part` orphan (left behind by a crashed writer)
24
- * is eligible for GC. Default 10 minutes. */
25
- orphanGraceMs: number
26
- }
27
-
28
- const DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1000
29
- const DEFAULT_ORPHAN_GRACE_MS = 10 * 60 * 1000
30
-
31
- function defaultRoot(): string {
32
- return path.join(os.homedir(), '.claude', 'im-downloads')
33
- }
34
-
35
- /** Strip path separators / .. / control chars from a filename. */
36
- function sanitizeFilename(name: string): string {
37
- // eslint-disable-next-line no-control-regex
38
- const base = path.basename(name || '').replace(/[\x00-\x1f]/g, '')
39
- const cleaned = base.replace(/[\/\\]/g, '_').replace(/\.\.+/g, '_')
40
- return cleaned.trim() || 'unnamed'
41
- }
42
-
43
- export class AttachmentStore {
44
- private readonly root: string
45
- private readonly retentionMs: number
46
- private readonly orphanGraceMs: number
47
-
48
- constructor(config?: Partial<AttachmentStoreConfig>) {
49
- this.root = config?.root ?? defaultRoot()
50
- this.retentionMs = config?.retentionMs ?? DEFAULT_RETENTION_MS
51
- this.orphanGraceMs = config?.orphanGraceMs ?? DEFAULT_ORPHAN_GRACE_MS
52
- }
53
-
54
- /** Compute the target path. Creates parent dirs on demand.
55
- * If a file with the same name already exists, prefix with a timestamp
56
- * to avoid clobbering. */
57
- resolvePath(platform: ImPlatform, sessionId: string, name: string): string {
58
- const safeSession = sanitizeFilename(sessionId)
59
- const dir = path.join(this.root, platform, safeSession)
60
- fsSync.mkdirSync(dir, { recursive: true })
61
- const safeName = sanitizeFilename(name)
62
- const candidate = path.join(dir, safeName)
63
- if (!fsSync.existsSync(candidate)) return candidate
64
- const { name: base, ext } = path.parse(safeName)
65
- // Collisions are rare in practice, but multiple downloads landing in the
66
- // same millisecond must still produce unique paths — append a random
67
- // suffix so the bare timestamp alone never clashes.
68
- const rand = Math.random().toString(36).slice(2, 8)
69
- return path.join(dir, `${base}-${Date.now()}-${rand}${ext}`)
70
- }
71
-
72
- /** Write atomically: stream to {target}.part, then rename. */
73
- async write(target: string, data: Buffer): Promise<string> {
74
- await fs.mkdir(path.dirname(target), { recursive: true })
75
- const tmp = `${target}.${process.pid}.${Date.now()}.part`
76
- await fs.writeFile(tmp, data)
77
- await fs.rename(tmp, target)
78
- return target
79
- }
80
-
81
- /** Remove files older than retentionMs. Returns summary. */
82
- async gc(): Promise<{ removed: number; bytes: number }> {
83
- let removed = 0
84
- let bytes = 0
85
- const now = Date.now()
86
-
87
- const walk = async (dir: string): Promise<void> => {
88
- let entries: Dirent<string>[]
89
- try {
90
- // Pass encoding explicitly so Dirent stays string-typed under
91
- // newer @types/node where the Buffer overload becomes the default.
92
- entries = await fs.readdir(dir, { withFileTypes: true, encoding: 'utf8' })
93
- } catch {
94
- return
95
- }
96
- for (const entry of entries) {
97
- const full = path.join(dir, entry.name)
98
- if (entry.isDirectory()) {
99
- await walk(full)
100
- } else if (entry.isFile()) {
101
- try {
102
- const stat = await fs.stat(full)
103
- const age = now - stat.mtimeMs
104
- const isOrphanPart = entry.name.endsWith('.part')
105
- const threshold = isOrphanPart ? this.orphanGraceMs : this.retentionMs
106
- if (age > threshold) {
107
- bytes += stat.size
108
- await fs.unlink(full)
109
- removed++
110
- }
111
- } catch {
112
- // ignore races
113
- }
114
- }
115
- }
116
- }
117
-
118
- await walk(this.root).catch(() => {})
119
- return { removed, bytes }
120
- }
121
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Shared attachment types for IM adapters.
3
- */
4
-
5
- import type { AttachmentRef } from '../ws-bridge.js'
6
- export type { AttachmentRef }
7
-
8
- /** Platform tag — used for local staging subdir and telemetry. */
9
- export type ImPlatform = 'feishu' | 'telegram'
10
-
11
- /** Result of downloading an IM resource into the local stage dir. */
12
- export interface LocalAttachment {
13
- kind: 'image' | 'file'
14
- name: string // original filename, or synthesized if none
15
- path: string // absolute path on disk (under ~/.claude/im-downloads)
16
- size: number // bytes
17
- mimeType: string // detected or provided
18
- buffer: Buffer // raw bytes (kept so caller can choose base64 vs path)
19
- }
20
-
21
- /** Pending outbound media found in Agent stream output. */
22
- export interface PendingUpload {
23
- id: string // fingerprint, used for dedup
24
- source:
25
- | { kind: 'base64'; data: string; mime: string }
26
- | { kind: 'path'; path: string; mime?: string }
27
- | { kind: 'url'; url: string; mime?: string }
28
- alt?: string
29
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * Pure, stateful extractor that watches a stream of assistant text for
3
- * markdown image references (`![alt](source)`) 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: ![alt](target)
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
- }