bingocode 1.0.27 → 1.0.29

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 (54) hide show
  1. package/package.json +1 -2
  2. package/.github/FUNDING.yml +0 -1
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -44
  4. package/.github/ISSUE_TEMPLATE/config.yml +0 -1
  5. package/.github/ISSUE_TEMPLATE/question.md +0 -40
  6. package/.github/workflows/build-desktop-dev.yml +0 -210
  7. package/.github/workflows/deploy-docs.yml +0 -59
  8. package/.github/workflows/release-desktop.yml +0 -162
  9. package/.spine/user.yaml +0 -5
  10. package/.spine/workspace.yaml +0 -1
  11. package/adapters/common/__tests__/chat-queue.test.ts +0 -61
  12. package/adapters/common/__tests__/format.test.ts +0 -148
  13. package/adapters/common/__tests__/http-client.test.ts +0 -105
  14. package/adapters/common/__tests__/message-buffer.test.ts +0 -84
  15. package/adapters/common/__tests__/message-dedup.test.ts +0 -57
  16. package/adapters/common/__tests__/session-store.test.ts +0 -62
  17. package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
  18. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
  19. package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
  20. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
  21. package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
  22. package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
  23. package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
  24. package/adapters/feishu/__tests__/feishu.test.ts +0 -907
  25. package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
  26. package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
  27. package/adapters/feishu/__tests__/media.test.ts +0 -120
  28. package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
  29. package/adapters/telegram/__tests__/media.test.ts +0 -86
  30. package/adapters/telegram/__tests__/telegram.test.ts +0 -115
  31. package/src/server/__tests__/conversation-service.test.ts +0 -173
  32. package/src/server/__tests__/conversations.test.ts +0 -458
  33. package/src/server/__tests__/cron-scheduler.test.ts +0 -575
  34. package/src/server/__tests__/e2e/business-flow.test.ts +0 -841
  35. package/src/server/__tests__/e2e/full-flow.test.ts +0 -357
  36. package/src/server/__tests__/fixtures/mock-sdk-cli.ts +0 -123
  37. package/src/server/__tests__/haha-oauth-api.test.ts +0 -146
  38. package/src/server/__tests__/haha-oauth-service.test.ts +0 -185
  39. package/src/server/__tests__/providers-real.test.ts +0 -244
  40. package/src/server/__tests__/providers.test.ts +0 -579
  41. package/src/server/__tests__/proxy-streaming.test.ts +0 -317
  42. package/src/server/__tests__/proxy-transform.test.ts +0 -469
  43. package/src/server/__tests__/real-llm-test.ts +0 -526
  44. package/src/server/__tests__/scheduled-tasks.test.ts +0 -371
  45. package/src/server/__tests__/sessions.test.ts +0 -786
  46. package/src/server/__tests__/settings.test.ts +0 -376
  47. package/src/server/__tests__/skills.test.ts +0 -125
  48. package/src/server/__tests__/tasks.test.ts +0 -171
  49. package/src/server/__tests__/team-watcher.test.ts +0 -400
  50. package/src/server/__tests__/teams.test.ts +0 -627
  51. package/src/server/middleware/cors.test.ts +0 -27
  52. package/src/utils/__tests__/cronFrequency.test.ts +0 -153
  53. package/src/utils/__tests__/cronTasks.test.ts +0 -204
  54. package/src/utils/computerUse/permissions.test.ts +0 -44
@@ -1,185 +0,0 @@
1
- /**
2
- * Unit tests for HahaOAuthService — haha 自管 OAuth 的核心 service 层。
3
- */
4
-
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
6
- import * as fs from 'fs/promises'
7
- import * as path from 'path'
8
- import * as os from 'os'
9
- import {
10
- HahaOAuthService,
11
- type StoredOAuthTokens,
12
- } from '../services/hahaOAuthService.js'
13
-
14
- let tmpDir: string
15
- let originalConfigDir: string | undefined
16
- let service: HahaOAuthService
17
-
18
- async function setup() {
19
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'haha-oauth-test-'))
20
- originalConfigDir = process.env.CLAUDE_CONFIG_DIR
21
- process.env.CLAUDE_CONFIG_DIR = tmpDir
22
- service = new HahaOAuthService()
23
- }
24
-
25
- async function teardown() {
26
- if (originalConfigDir === undefined) {
27
- delete process.env.CLAUDE_CONFIG_DIR
28
- } else {
29
- process.env.CLAUDE_CONFIG_DIR = originalConfigDir
30
- }
31
- await fs.rm(tmpDir, { recursive: true, force: true })
32
- }
33
-
34
- describe('HahaOAuthService — file storage', () => {
35
- beforeEach(setup)
36
- afterEach(teardown)
37
-
38
- test('loadTokens returns null when file does not exist', async () => {
39
- expect(await service.loadTokens()).toBeNull()
40
- })
41
-
42
- test('saveTokens writes file with 0600 permissions', async () => {
43
- const tokens: StoredOAuthTokens = {
44
- accessToken: 'sk-ant-oat01-xxx',
45
- refreshToken: 'sk-ant-ort01-xxx',
46
- expiresAt: Date.now() + 3600_000,
47
- scopes: ['user:inference', 'user:profile'],
48
- subscriptionType: 'max',
49
- }
50
- await service.saveTokens(tokens)
51
-
52
- const oauthPath = path.join(tmpDir, 'bingo', 'oauth.json')
53
- const stat = await fs.stat(oauthPath)
54
- expect(stat.mode & 0o777).toBe(0o600)
55
-
56
- const loaded = await service.loadTokens()
57
- expect(loaded).toEqual(tokens)
58
- })
59
-
60
- test('deleteTokens removes file', async () => {
61
- await service.saveTokens({
62
- accessToken: 'a',
63
- refreshToken: null,
64
- expiresAt: null,
65
- scopes: [],
66
- subscriptionType: null,
67
- })
68
- await service.deleteTokens()
69
- expect(await service.loadTokens()).toBeNull()
70
- })
71
- })
72
-
73
- describe('HahaOAuthService — session management', () => {
74
- beforeEach(setup)
75
- afterEach(teardown)
76
-
77
- test('startSession creates session with PKCE + state', () => {
78
- const session = service.startSession({ serverPort: 54321 })
79
- expect(session.state).toMatch(/^[A-Za-z0-9_-]{43}$/)
80
- expect(session.codeVerifier).toMatch(/^[A-Za-z0-9_-]{43}$/)
81
- expect(session.authorizeUrl).toContain('code_challenge_method=S256')
82
- expect(session.authorizeUrl).toContain(`state=${encodeURIComponent(session.state)}`)
83
- expect(session.authorizeUrl).toContain('redirect_uri=')
84
- expect(session.authorizeUrl).toContain(encodeURIComponent(
85
- 'http://localhost:54321/callback',
86
- ))
87
- })
88
-
89
- test('getSession returns stored session by state', () => {
90
- const session = service.startSession({ serverPort: 54321 })
91
- const found = service.getSession(session.state)
92
- expect(found?.codeVerifier).toBe(session.codeVerifier)
93
- })
94
-
95
- test('getSession returns null for unknown state', () => {
96
- expect(service.getSession('unknown-state')).toBeNull()
97
- })
98
-
99
- test('consumeSession removes session after fetch', () => {
100
- const session = service.startSession({ serverPort: 54321 })
101
- expect(service.consumeSession(session.state)).not.toBeNull()
102
- expect(service.getSession(session.state)).toBeNull()
103
- })
104
-
105
- test('completeSession stores subscription type fetched from profile info', async () => {
106
- const session = service.startSession({ serverPort: 54321 })
107
- ;(service as any).exchangeWithCustomCallback = async () => ({
108
- access_token: 'fresh-access-token',
109
- refresh_token: 'fresh-refresh-token',
110
- expires_in: 3600,
111
- scope: 'user:inference',
112
- })
113
- service.setFetchProfileFn(async () => ({
114
- subscriptionType: 'team',
115
- }))
116
-
117
- const tokens = await service.completeSession('authorization-code', session.state)
118
-
119
- expect(tokens.subscriptionType).toBe('team')
120
- expect((await service.loadTokens())?.subscriptionType).toBe('team')
121
- })
122
- })
123
-
124
- describe('HahaOAuthService — ensureFreshAccessToken', () => {
125
- beforeEach(setup)
126
- afterEach(teardown)
127
-
128
- test('returns null when no token file exists', async () => {
129
- expect(await service.ensureFreshAccessToken()).toBeNull()
130
- })
131
-
132
- test('returns token unchanged if not expired', async () => {
133
- const tokens: StoredOAuthTokens = {
134
- accessToken: 'still-valid',
135
- refreshToken: 'refresh-xxx',
136
- expiresAt: Date.now() + 30 * 60_000,
137
- scopes: ['user:inference'],
138
- subscriptionType: 'max',
139
- }
140
- await service.saveTokens(tokens)
141
-
142
- expect(await service.ensureFreshAccessToken()).toBe('still-valid')
143
- })
144
-
145
- test('refreshes token when expired (within 5-min buffer)', async () => {
146
- const oldTokens: StoredOAuthTokens = {
147
- accessToken: 'expired',
148
- refreshToken: 'refresh-xxx',
149
- expiresAt: Date.now() + 60_000,
150
- scopes: ['user:inference'],
151
- subscriptionType: 'max',
152
- }
153
- await service.saveTokens(oldTokens)
154
-
155
- service.setRefreshFn(async () => ({
156
- accessToken: 'new-fresh-token',
157
- refreshToken: 'new-refresh-xxx',
158
- expiresAt: Date.now() + 3600_000,
159
- scopes: ['user:inference'],
160
- subscriptionType: 'max',
161
- rateLimitTier: null,
162
- }))
163
-
164
- const fresh = await service.ensureFreshAccessToken()
165
- expect(fresh).toBe('new-fresh-token')
166
-
167
- const loaded = await service.loadTokens()
168
- expect(loaded?.accessToken).toBe('new-fresh-token')
169
- })
170
-
171
- test('returns null when refresh fails', async () => {
172
- await service.saveTokens({
173
- accessToken: 'expired',
174
- refreshToken: 'bad-refresh',
175
- expiresAt: Date.now() + 60_000,
176
- scopes: ['user:inference'],
177
- subscriptionType: null,
178
- })
179
- service.setRefreshFn(async () => {
180
- throw new Error('401 Unauthorized')
181
- })
182
-
183
- expect(await service.ensureFreshAccessToken()).toBeNull()
184
- })
185
- })
@@ -1,244 +0,0 @@
1
- /**
2
- * 用真实的 Provider 配置测试 ProviderService
3
- * 验证添加、激活、bingo/settings.json 同步是否正确
4
- * (provider env 写到 ~/.claude/bingo/settings.json,不污染原版 settings.json)
5
- */
6
-
7
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
8
- import * as fs from 'fs/promises'
9
- import * as path from 'path'
10
- import * as os from 'os'
11
- import { ProviderService } from '../services/providerService.js'
12
-
13
- const MODEL_MAPPING = {
14
- main: 'MiniMax-M2.7-highspeed',
15
- haiku: 'MiniMax-M2.7-highspeed',
16
- sonnet: 'MiniMax-M2.7-highspeed',
17
- opus: 'MiniMax-M2.7-highspeed',
18
- }
19
-
20
- describe('Real Provider Configs', () => {
21
- let tmpDir: string
22
- let service: ProviderService
23
-
24
- beforeEach(async () => {
25
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'provider-real-'))
26
- process.env.CLAUDE_CONFIG_DIR = tmpDir
27
- service = new ProviderService()
28
- })
29
-
30
- afterEach(async () => {
31
- delete process.env.CLAUDE_CONFIG_DIR
32
- await fs.rm(tmpDir, { recursive: true, force: true })
33
- })
34
-
35
- // Helper: read the Haha-specific settings file
36
- async function readCcHahaSettings(): Promise<Record<string, unknown>> {
37
- const raw = await fs.readFile(path.join(tmpDir, 'bingo', 'settings.json'), 'utf-8')
38
- return JSON.parse(raw)
39
- }
40
-
41
- // Helper: check original settings.json is NOT modified
42
- async function originalSettingsExists(): Promise<boolean> {
43
- try {
44
- await fs.access(path.join(tmpDir, 'settings.json'))
45
- return true
46
- } catch {
47
- return false
48
- }
49
- }
50
-
51
- test('添加 MiniMax Provider 并激活 — 写入 bingo/settings.json', async () => {
52
- const minimax = await service.addProvider({
53
- presetId: 'minimax',
54
- name: 'MiniMax',
55
- baseUrl: 'https://api.minimaxi.com/anthropic',
56
- apiKey: 'sk-fake-test-key-for-testing-only',
57
- models: MODEL_MAPPING,
58
- notes: 'MiniMax 官方 Anthropic 兼容接口',
59
- })
60
-
61
- expect(minimax.name).toBe('MiniMax')
62
-
63
- // 激活 provider
64
- await service.activateProvider(minimax.id)
65
-
66
- // 验证写入 bingo/settings.json
67
- const settings = await readCcHahaSettings()
68
- expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
69
- expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-fake-test-key-for-testing-only')
70
- expect((settings.env as Record<string, string>).ANTHROPIC_MODEL).toBe('MiniMax-M2.7-highspeed')
71
-
72
- // 验证原版 settings.json 没有被创建
73
- expect(await originalSettingsExists()).toBe(false)
74
-
75
- console.log('✅ Provider 写入 bingo/settings.json,原版 settings.json 未被污染')
76
- })
77
-
78
- test('切换 Provider — 更新 bingo/settings.json', async () => {
79
- const minimax = await service.addProvider({
80
- presetId: 'minimax',
81
- name: 'MiniMax',
82
- baseUrl: 'https://api.minimaxi.com/anthropic',
83
- apiKey: 'sk-api-test-minimax',
84
- models: MODEL_MAPPING,
85
- })
86
-
87
- const jiekou = await service.addProvider({
88
- presetId: 'custom',
89
- name: '接口AI中转站',
90
- baseUrl: 'https://api.jiekou.ai/anthropic',
91
- apiKey: 'sk-fake-test-key-for-testing-only',
92
- models: {
93
- main: 'claude-opus-4-7',
94
- haiku: 'claude-haiku-4-5',
95
- sonnet: 'claude-sonnet-4-6',
96
- opus: 'claude-opus-4-7',
97
- },
98
- })
99
-
100
- // 先激活 MiniMax
101
- await service.activateProvider(minimax.id)
102
- let settings = await readCcHahaSettings()
103
- expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
104
-
105
- // 切换到接口AI中转站
106
- await service.activateProvider(jiekou.id)
107
- settings = await readCcHahaSettings()
108
- expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.jiekou.ai/anthropic')
109
- expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-fake-test-key-for-testing-only')
110
- expect((settings.env as Record<string, string>).ANTHROPIC_MODEL).toBe('claude-opus-4-7')
111
-
112
- // 验证 activeId 正确
113
- const list = await service.listProviders()
114
- expect(list.activeId).toBe(jiekou.id)
115
-
116
- // 原版 settings.json 依然不存在
117
- expect(await originalSettingsExists()).toBe(false)
118
-
119
- console.log('✅ 切换 Provider 成功,bingo/settings.json 更新正确')
120
- })
121
-
122
- test('bingo/settings.json 保留已有字段', async () => {
123
- // 预写一个有内容的 bingo/settings.json(模拟用户已有配置)
124
- await fs.mkdir(path.join(tmpDir, 'bingo'), { recursive: true })
125
- await fs.writeFile(
126
- path.join(tmpDir, 'bingo', 'settings.json'),
127
- JSON.stringify({
128
- customField: 'should_be_preserved',
129
- env: {
130
- EXISTING_VAR: 'should_be_preserved',
131
- },
132
- }, null, 2),
133
- )
134
-
135
- // 添加并激活 provider
136
- const provider = await service.addProvider({
137
- presetId: 'custom',
138
- name: '接口AI中转站',
139
- baseUrl: 'https://api.jiekou.ai/anthropic',
140
- apiKey: 'sk_test',
141
- models: {
142
- main: 'claude-opus-4-7',
143
- haiku: 'claude-haiku-4-5',
144
- sonnet: 'claude-sonnet-4-6',
145
- opus: 'claude-opus-4-7',
146
- },
147
- })
148
- await service.activateProvider(provider.id)
149
-
150
- const settings = await readCcHahaSettings()
151
-
152
- // 验证新字段写入
153
- expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.jiekou.ai/anthropic')
154
- expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk_test')
155
-
156
- // 验证已有字段保留
157
- expect(settings.customField).toBe('should_be_preserved')
158
- expect((settings.env as Record<string, string>).EXISTING_VAR).toBe('should_be_preserved')
159
-
160
- console.log('✅ bingo/settings.json 已有字段全部保留')
161
- })
162
-
163
- test('activateOfficial 清除 provider env', async () => {
164
- const provider = await service.addProvider({
165
- presetId: 'minimax',
166
- name: 'MiniMax',
167
- baseUrl: 'https://api.minimaxi.com/anthropic',
168
- apiKey: 'sk-test',
169
- models: MODEL_MAPPING,
170
- })
171
-
172
- await service.activateProvider(provider.id)
173
-
174
- // 确认写入了
175
- let settings = await readCcHahaSettings()
176
- expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBeDefined()
177
-
178
- // 切换到 official
179
- await service.activateOfficial()
180
-
181
- settings = await readCcHahaSettings()
182
- const env = settings.env as Record<string, string> | undefined
183
- expect(env?.ANTHROPIC_BASE_URL).toBeUndefined()
184
- expect(env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
185
- expect(env?.ANTHROPIC_MODEL).toBeUndefined()
186
-
187
- console.log('✅ activateOfficial 正确清除了 provider env')
188
- })
189
-
190
- test('连通性测试 — 返回结构正确', async () => {
191
- const result = await service.testProviderConfig({
192
- baseUrl: 'https://api.minimaxi.com/anthropic',
193
- apiKey: 'sk-fake-test-key',
194
- modelId: 'MiniMax-M2.7-highspeed',
195
- })
196
-
197
- // testProviderConfig 返回 { connectivity: { ... }, proxy?: { ... } }
198
- expect(result.connectivity).toBeDefined()
199
- expect(result.connectivity.latencyMs).toBeGreaterThanOrEqual(0)
200
- expect(result.connectivity.modelUsed).toBe('MiniMax-M2.7-highspeed')
201
-
202
- console.log('🔌 MiniMax 连通性测试结果:')
203
- console.log(' success:', result.connectivity.success)
204
- console.log(' latencyMs:', result.connectivity.latencyMs)
205
- console.log(' error:', result.connectivity.error)
206
- })
207
-
208
- test('providers.json 和 bingo/settings.json 独立于 settings.json', async () => {
209
- // 模拟原版 Claude Code 的 settings.json 已存在
210
- await fs.writeFile(
211
- path.join(tmpDir, 'settings.json'),
212
- JSON.stringify({
213
- effortLevel: 'high',
214
- env: {
215
- ANTHROPIC_BASE_URL: 'https://original-claude-code.api.com',
216
- ANTHROPIC_AUTH_TOKEN: 'original-key',
217
- },
218
- }, null, 2),
219
- )
220
-
221
- // Haha 添加并激活自己的 provider
222
- const provider = await service.addProvider({
223
- presetId: 'minimax',
224
- name: 'MiniMax',
225
- baseUrl: 'https://api.minimaxi.com/anthropic',
226
- apiKey: 'sk-haha-key',
227
- models: MODEL_MAPPING,
228
- })
229
- await service.activateProvider(provider.id)
230
-
231
- // 验证原版 settings.json 没被修改
232
- const original = JSON.parse(await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8'))
233
- expect((original.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://original-claude-code.api.com')
234
- expect((original.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('original-key')
235
- expect(original.effortLevel).toBe('high')
236
-
237
- // 验证 bingo/settings.json 是 Haha 自己的
238
- const haha = await readCcHahaSettings()
239
- expect((haha.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
240
- expect((haha.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-haha-key')
241
-
242
- console.log('✅ 原版 settings.json 完好无损,Haha 配置独立存储')
243
- })
244
- })